source: josm/trunk/src/org/openstreetmap/josm/io/NameFinder.java@ 16419

Last change on this file since 16419 was 16419, checked in by simon04, 4 years ago

fix #19099 - PlaceSelection: search for more results (Nominatim)

After the initial search, the button changes to "Search more..."

File size: 11.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.IOException;
7import java.io.Reader;
8import java.io.UncheckedIOException;
9import java.net.MalformedURLException;
10import java.net.URL;
11import java.util.Collection;
12import java.util.Collections;
13import java.util.LinkedList;
14import java.util.List;
15import java.util.Optional;
16import java.util.stream.Collectors;
17
18import javax.xml.parsers.ParserConfigurationException;
19
20import org.openstreetmap.josm.data.Bounds;
21import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
22import org.openstreetmap.josm.data.osm.PrimitiveId;
23import org.openstreetmap.josm.data.osm.SimplePrimitiveId;
24import org.openstreetmap.josm.data.preferences.StringProperty;
25import org.openstreetmap.josm.tools.HttpClient;
26import org.openstreetmap.josm.tools.HttpClient.Response;
27import org.openstreetmap.josm.tools.Logging;
28import org.openstreetmap.josm.tools.OsmUrlToBounds;
29import org.openstreetmap.josm.tools.UncheckedParseException;
30import org.openstreetmap.josm.tools.Utils;
31import org.openstreetmap.josm.tools.XmlUtils;
32import org.xml.sax.Attributes;
33import org.xml.sax.InputSource;
34import org.xml.sax.SAXException;
35import org.xml.sax.helpers.DefaultHandler;
36
37/**
38 * Search for names and related items.
39 * @since 11002
40 */
41public final class NameFinder {
42
43 /**
44 * Nominatim default URL.
45 */
46 public static final String NOMINATIM_URL = "https://nominatim.openstreetmap.org/search?format=xml&q=";
47
48 /**
49 * Nominatim URL property.
50 * @since 12557
51 */
52 public static final StringProperty NOMINATIM_URL_PROP = new StringProperty("nominatim-url", NOMINATIM_URL);
53
54 private NameFinder() {
55 }
56
57 /**
58 * Builds the Nominatim URL for performing the given search
59 * @param searchExpression the Nominatim query
60 * @return the Nominatim URL
61 */
62 public static URL buildNominatimURL(String searchExpression) {
63 return buildNominatimURL(searchExpression, Collections.emptyList());
64 }
65
66 /**
67 * Builds the Nominatim URL for performing the given search and excluding the results (of a previous search)
68 * @param searchExpression the Nominatim query
69 * @param excludeResults the results to exclude
70 * @return the Nominatim URL
71 * @see <a href="https://nominatim.org/release-docs/develop/api/Search/#result-limitation">Result limitation in Nominatim Documentation</a>
72 */
73 public static URL buildNominatimURL(String searchExpression, Collection<SearchResult> excludeResults) {
74 try {
75 final String excludeString = excludeResults.isEmpty()
76 ? ""
77 : excludeResults.stream()
78 .map(SearchResult::getPlaceId)
79 .map(String::valueOf)
80 .collect(Collectors.joining(",", "&exclude_place_ids=", ""));
81 return new URL(NOMINATIM_URL_PROP.get() + Utils.encodeUrl(searchExpression) + excludeString);
82 } catch (MalformedURLException ex) {
83 throw new UncheckedIOException(ex);
84 }
85 }
86
87 /**
88 * Performs a Nominatim search.
89 * @param searchExpression Nominatim search expression
90 * @return search results
91 * @throws IOException if any IO error occurs.
92 */
93 public static List<SearchResult> queryNominatim(final String searchExpression) throws IOException {
94 return query(buildNominatimURL(searchExpression));
95 }
96
97 /**
98 * Performs a custom search.
99 * @param url search URL to any Nominatim instance
100 * @return search results
101 * @throws IOException if any IO error occurs.
102 */
103 public static List<SearchResult> query(final URL url) throws IOException {
104 final HttpClient connection = HttpClient.create(url)
105 .setAccept("application/xml, */*;q=0.8");
106 Response response = connection.connect();
107 if (response.getResponseCode() >= 400) {
108 throw new IOException(response.getResponseMessage() + ": " + response.fetchContent());
109 }
110 try (Reader reader = response.getContentReader()) {
111 return parseSearchResults(reader);
112 } catch (ParserConfigurationException | SAXException ex) {
113 throw new UncheckedParseException(ex);
114 }
115 }
116
117 /**
118 * Parse search results as returned by Nominatim.
119 * @param reader reader
120 * @return search results
121 * @throws ParserConfigurationException if a parser cannot be created which satisfies the requested configuration.
122 * @throws SAXException for SAX errors.
123 * @throws IOException if any IO error occurs.
124 */
125 public static List<SearchResult> parseSearchResults(Reader reader) throws IOException, ParserConfigurationException, SAXException {
126 InputSource inputSource = new InputSource(reader);
127 NameFinderResultParser parser = new NameFinderResultParser();
128 XmlUtils.parseSafeSAX(inputSource, parser);
129 return parser.getResult();
130 }
131
132 /**
133 * Data storage for search results.
134 */
135 public static class SearchResult {
136 private String name;
137 private String info;
138 private String nearestPlace;
139 private String description;
140 private double lat;
141 private double lon;
142 private int zoom;
143 private Bounds bounds;
144 private PrimitiveId osmId;
145 private long placeId;
146
147 /**
148 * Returns the name.
149 * @return the name
150 */
151 public final String getName() {
152 return name;
153 }
154
155 /**
156 * Returns the info.
157 * @return the info
158 */
159 public final String getInfo() {
160 return info;
161 }
162
163 /**
164 * Returns the nearest place.
165 * @return the nearest place
166 */
167 public final String getNearestPlace() {
168 return nearestPlace;
169 }
170
171 /**
172 * Returns the description.
173 * @return the description
174 */
175 public final String getDescription() {
176 return description;
177 }
178
179 /**
180 * Returns the latitude.
181 * @return the latitude
182 */
183 public final double getLat() {
184 return lat;
185 }
186
187 /**
188 * Returns the longitude.
189 * @return the longitude
190 */
191 public final double getLon() {
192 return lon;
193 }
194
195 /**
196 * Returns the zoom.
197 * @return the zoom
198 */
199 public final int getZoom() {
200 return zoom;
201 }
202
203 /**
204 * Returns the bounds.
205 * @return the bounds
206 */
207 public final Bounds getBounds() {
208 return bounds;
209 }
210
211 /**
212 * Returns the OSM id.
213 * @return the OSM id
214 */
215 public final PrimitiveId getOsmId() {
216 return osmId;
217 }
218
219 /**
220 * Returns the Nominatim place id.
221 * @return the Nominatim place id
222 */
223 public long getPlaceId() {
224 return placeId;
225 }
226
227 /**
228 * Returns the download area.
229 * @return the download area
230 */
231 public Bounds getDownloadArea() {
232 return bounds != null ? bounds : OsmUrlToBounds.positionToBounds(lat, lon, zoom);
233 }
234 }
235
236 /**
237 * A very primitive parser for the name finder's output.
238 * Structure of xml described here: http://wiki.openstreetmap.org/index.php/Name_finder
239 */
240 private static class NameFinderResultParser extends DefaultHandler {
241 private SearchResult currentResult;
242 private StringBuilder description;
243 private int depth;
244 private final List<SearchResult> data = new LinkedList<>();
245
246 /**
247 * Detect starting elements.
248 */
249 @Override
250 public void startElement(String namespaceURI, String localName, String qName, Attributes atts)
251 throws SAXException {
252 depth++;
253 try {
254 if ("searchresults".equals(qName)) {
255 // do nothing
256 } else if (depth == 2 && "named".equals(qName)) {
257 currentResult = new SearchResult();
258 currentResult.name = atts.getValue("name");
259 currentResult.info = atts.getValue("info");
260 if (currentResult.info != null) {
261 currentResult.info = tr(currentResult.info);
262 }
263 currentResult.lat = Double.parseDouble(atts.getValue("lat"));
264 currentResult.lon = Double.parseDouble(atts.getValue("lon"));
265 currentResult.zoom = Integer.parseInt(atts.getValue("zoom"));
266 data.add(currentResult);
267 } else if (depth == 3 && "description".equals(qName)) {
268 description = new StringBuilder();
269 } else if (depth == 4 && "named".equals(qName)) {
270 // this is a "named" place in the nearest places list.
271 String info = atts.getValue("info");
272 if ("city".equals(info) || "town".equals(info) || "village".equals(info)) {
273 currentResult.nearestPlace = atts.getValue("name");
274 }
275 } else if ("place".equals(qName) && atts.getValue("lat") != null) {
276 currentResult = new SearchResult();
277 currentResult.name = atts.getValue("display_name");
278 currentResult.description = currentResult.name;
279 currentResult.info = atts.getValue("class");
280 if (currentResult.info != null) {
281 currentResult.info = tr(currentResult.info);
282 }
283 currentResult.nearestPlace = tr(atts.getValue("type"));
284 currentResult.lat = Double.parseDouble(atts.getValue("lat"));
285 currentResult.lon = Double.parseDouble(atts.getValue("lon"));
286 String[] bbox = atts.getValue("boundingbox").split(",");
287 currentResult.bounds = new Bounds(
288 Double.parseDouble(bbox[0]), Double.parseDouble(bbox[2]),
289 Double.parseDouble(bbox[1]), Double.parseDouble(bbox[3]));
290 final String osmId = atts.getValue("osm_id");
291 final String osmType = atts.getValue("osm_type");
292 if (osmId != null && osmType != null) {
293 currentResult.osmId = new SimplePrimitiveId(Long.parseLong(osmId), OsmPrimitiveType.from(osmType));
294 }
295 currentResult.placeId = Optional.ofNullable(atts.getValue("place_id")).filter(s -> !s.isEmpty())
296 .map(Long::parseLong).orElse(0L);
297 data.add(currentResult);
298 }
299 } catch (NumberFormatException ex) {
300 Logging.error(ex); // SAXException does not chain correctly
301 throw new SAXException(ex.getMessage(), ex);
302 } catch (NullPointerException ex) { // NOPMD
303 Logging.error(ex); // SAXException does not chain correctly
304 throw new SAXException(tr("Null pointer exception, possibly some missing tags."), ex);
305 }
306 }
307
308 /**
309 * Detect ending elements.
310 */
311 @Override
312 public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
313 if (description != null && "description".equals(qName)) {
314 currentResult.description = description.toString();
315 description = null;
316 }
317 depth--;
318 }
319
320 /**
321 * Read characters for description.
322 */
323 @Override
324 public void characters(char[] data, int start, int length) throws SAXException {
325 if (description != null) {
326 description.append(data, start, length);
327 }
328 }
329
330 public List<SearchResult> getResult() {
331 return Collections.unmodifiableList(data);
332 }
333 }
334}
Note: See TracBrowser for help on using the repository browser.