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