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}