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