001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.io; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.io.IOException; 007import java.io.Reader; 008import java.net.URL; 009import java.util.Collections; 010import java.util.LinkedList; 011import java.util.List; 012 013import javax.xml.parsers.ParserConfigurationException; 014 015import org.openstreetmap.josm.data.Bounds; 016import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 017import org.openstreetmap.josm.data.osm.PrimitiveId; 018import org.openstreetmap.josm.data.osm.SimplePrimitiveId; 019import org.openstreetmap.josm.data.preferences.StringProperty; 020import org.openstreetmap.josm.tools.HttpClient; 021import org.openstreetmap.josm.tools.HttpClient.Response; 022import org.openstreetmap.josm.tools.Logging; 023import org.openstreetmap.josm.tools.OsmUrlToBounds; 024import org.openstreetmap.josm.tools.UncheckedParseException; 025import org.openstreetmap.josm.tools.Utils; 026import org.openstreetmap.josm.tools.XmlUtils; 027import org.xml.sax.Attributes; 028import org.xml.sax.InputSource; 029import org.xml.sax.SAXException; 030import org.xml.sax.helpers.DefaultHandler; 031 032/** 033 * Search for names and related items. 034 * @since 11002 035 */ 036public final class NameFinder { 037 038 /** 039 * Nominatim default URL. 040 */ 041 public static final String NOMINATIM_URL = "https://nominatim.openstreetmap.org/search?format=xml&q="; 042 043 /** 044 * Nominatim URL property. 045 * @since 12557 046 */ 047 public static final StringProperty NOMINATIM_URL_PROP = new StringProperty("nominatim-url", NOMINATIM_URL); 048 049 private NameFinder() { 050 } 051 052 /** 053 * Performs a Nominatim search. 054 * @param searchExpression Nominatim search expression 055 * @return search results 056 * @throws IOException if any IO error occurs. 057 */ 058 public static List<SearchResult> queryNominatim(final String searchExpression) throws IOException { 059 return query(new URL(NOMINATIM_URL_PROP.get() + Utils.encodeUrl(searchExpression))); 060 } 061 062 /** 063 * Performs a custom search. 064 * @param url search URL to any Nominatim instance 065 * @return search results 066 * @throws IOException if any IO error occurs. 067 */ 068 public static List<SearchResult> query(final URL url) throws IOException { 069 final HttpClient connection = HttpClient.create(url); 070 Response response = connection.connect(); 071 if (response.getResponseCode() >= 400) { 072 throw new IOException(response.getResponseMessage() + ": " + response.fetchContent()); 073 } 074 try (Reader reader = response.getContentReader()) { 075 return parseSearchResults(reader); 076 } catch (ParserConfigurationException | SAXException ex) { 077 throw new UncheckedParseException(ex); 078 } 079 } 080 081 /** 082 * Parse search results as returned by Nominatim. 083 * @param reader reader 084 * @return search results 085 * @throws ParserConfigurationException if a parser cannot be created which satisfies the requested configuration. 086 * @throws SAXException for SAX errors. 087 * @throws IOException if any IO error occurs. 088 */ 089 public static List<SearchResult> parseSearchResults(Reader reader) throws IOException, ParserConfigurationException, SAXException { 090 InputSource inputSource = new InputSource(reader); 091 NameFinderResultParser parser = new NameFinderResultParser(); 092 XmlUtils.parseSafeSAX(inputSource, parser); 093 return parser.getResult(); 094 } 095 096 /** 097 * Data storage for search results. 098 */ 099 public static class SearchResult { 100 private String name; 101 private String info; 102 private String nearestPlace; 103 private String description; 104 private double lat; 105 private double lon; 106 private int zoom; 107 private Bounds bounds; 108 private PrimitiveId osmId; 109 110 /** 111 * Returns the name. 112 * @return the name 113 */ 114 public final String getName() { 115 return name; 116 } 117 118 /** 119 * Returns the info. 120 * @return the info 121 */ 122 public final String getInfo() { 123 return info; 124 } 125 126 /** 127 * Returns the nearest place. 128 * @return the nearest place 129 */ 130 public final String getNearestPlace() { 131 return nearestPlace; 132 } 133 134 /** 135 * Returns the description. 136 * @return the description 137 */ 138 public final String getDescription() { 139 return description; 140 } 141 142 /** 143 * Returns the latitude. 144 * @return the latitude 145 */ 146 public final double getLat() { 147 return lat; 148 } 149 150 /** 151 * Returns the longitude. 152 * @return the longitude 153 */ 154 public final double getLon() { 155 return lon; 156 } 157 158 /** 159 * Returns the zoom. 160 * @return the zoom 161 */ 162 public final int getZoom() { 163 return zoom; 164 } 165 166 /** 167 * Returns the bounds. 168 * @return the bounds 169 */ 170 public final Bounds getBounds() { 171 return bounds; 172 } 173 174 /** 175 * Returns the OSM id. 176 * @return the OSM id 177 */ 178 public final PrimitiveId getOsmId() { 179 return osmId; 180 } 181 182 /** 183 * Returns the download area. 184 * @return the download area 185 */ 186 public Bounds getDownloadArea() { 187 return bounds != null ? bounds : OsmUrlToBounds.positionToBounds(lat, lon, zoom); 188 } 189 } 190 191 /** 192 * A very primitive parser for the name finder's output. 193 * Structure of xml described here: http://wiki.openstreetmap.org/index.php/Name_finder 194 */ 195 private static class NameFinderResultParser extends DefaultHandler { 196 private SearchResult currentResult; 197 private StringBuilder description; 198 private int depth; 199 private final List<SearchResult> data = new LinkedList<>(); 200 201 /** 202 * Detect starting elements. 203 */ 204 @Override 205 public void startElement(String namespaceURI, String localName, String qName, Attributes atts) 206 throws SAXException { 207 depth++; 208 try { 209 if ("searchresults".equals(qName)) { 210 // do nothing 211 } else if (depth == 2 && "named".equals(qName)) { 212 currentResult = new SearchResult(); 213 currentResult.name = atts.getValue("name"); 214 currentResult.info = atts.getValue("info"); 215 if (currentResult.info != null) { 216 currentResult.info = tr(currentResult.info); 217 } 218 currentResult.lat = Double.parseDouble(atts.getValue("lat")); 219 currentResult.lon = Double.parseDouble(atts.getValue("lon")); 220 currentResult.zoom = Integer.parseInt(atts.getValue("zoom")); 221 data.add(currentResult); 222 } else if (depth == 3 && "description".equals(qName)) { 223 description = new StringBuilder(); 224 } else if (depth == 4 && "named".equals(qName)) { 225 // this is a "named" place in the nearest places list. 226 String info = atts.getValue("info"); 227 if ("city".equals(info) || "town".equals(info) || "village".equals(info)) { 228 currentResult.nearestPlace = atts.getValue("name"); 229 } 230 } else if ("place".equals(qName) && atts.getValue("lat") != null) { 231 currentResult = new SearchResult(); 232 currentResult.name = atts.getValue("display_name"); 233 currentResult.description = currentResult.name; 234 currentResult.info = atts.getValue("class"); 235 if (currentResult.info != null) { 236 currentResult.info = tr(currentResult.info); 237 } 238 currentResult.nearestPlace = tr(atts.getValue("type")); 239 currentResult.lat = Double.parseDouble(atts.getValue("lat")); 240 currentResult.lon = Double.parseDouble(atts.getValue("lon")); 241 String[] bbox = atts.getValue("boundingbox").split(","); 242 currentResult.bounds = new Bounds( 243 Double.parseDouble(bbox[0]), Double.parseDouble(bbox[2]), 244 Double.parseDouble(bbox[1]), Double.parseDouble(bbox[3])); 245 final String osmId = atts.getValue("osm_id"); 246 final String osmType = atts.getValue("osm_type"); 247 if (osmId != null && osmType != null) { 248 currentResult.osmId = new SimplePrimitiveId(Long.parseLong(osmId), OsmPrimitiveType.from(osmType)); 249 } 250 data.add(currentResult); 251 } 252 } catch (NumberFormatException ex) { 253 Logging.error(ex); // SAXException does not chain correctly 254 throw new SAXException(ex.getMessage(), ex); 255 } catch (NullPointerException ex) { // NOPMD 256 Logging.error(ex); // SAXException does not chain correctly 257 throw new SAXException(tr("Null pointer exception, possibly some missing tags."), ex); 258 } 259 } 260 261 /** 262 * Detect ending elements. 263 */ 264 @Override 265 public void endElement(String namespaceURI, String localName, String qName) throws SAXException { 266 if (description != null && "description".equals(qName)) { 267 currentResult.description = description.toString(); 268 description = null; 269 } 270 depth--; 271 } 272 273 /** 274 * Read characters for description. 275 */ 276 @Override 277 public void characters(char[] data, int start, int length) throws SAXException { 278 if (description != null) { 279 description.append(data, start, length); 280 } 281 } 282 283 public List<SearchResult> getResult() { 284 return Collections.unmodifiableList(data); 285 } 286 } 287}