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}