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.InputStream;
008import java.util.EnumMap;
009import java.util.List;
010import java.util.NoSuchElementException;
011import java.util.concurrent.TimeUnit;
012import java.util.regex.Matcher;
013import java.util.regex.Pattern;
014
015import javax.xml.stream.XMLStreamConstants;
016import javax.xml.stream.XMLStreamException;
017
018import org.openstreetmap.josm.Main;
019import org.openstreetmap.josm.data.Bounds;
020import org.openstreetmap.josm.data.DataSource;
021import org.openstreetmap.josm.data.osm.DataSet;
022import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
023import org.openstreetmap.josm.data.osm.PrimitiveId;
024import org.openstreetmap.josm.gui.progress.ProgressMonitor;
025import org.openstreetmap.josm.tools.HttpClient;
026import org.openstreetmap.josm.tools.UncheckedParseException;
027import org.openstreetmap.josm.tools.Utils;
028
029/**
030 * Read content from an Overpass server.
031 *
032 * @since 8744
033 */
034public class OverpassDownloadReader extends BoundingBoxDownloader {
035
036    static final class OverpassOsmReader extends OsmReader {
037        @Override
038        protected void parseUnknown(boolean printWarning) throws XMLStreamException {
039            if ("remark".equals(parser.getLocalName()) && parser.getEventType() == XMLStreamConstants.START_ELEMENT) {
040                final String text = parser.getElementText();
041                if (text.contains("runtime error")) {
042                    throw new XMLStreamException(text);
043                }
044            }
045            super.parseUnknown(printWarning);
046        }
047    }
048
049    final String overpassServer;
050    final String overpassQuery;
051
052    /**
053     * Constructs a new {@code OverpassDownloadReader}.
054     *
055     * @param downloadArea   The area to download
056     * @param overpassServer The Overpass server to use
057     * @param overpassQuery  The Overpass query
058     */
059    public OverpassDownloadReader(Bounds downloadArea, String overpassServer, String overpassQuery) {
060        super(downloadArea);
061        this.overpassServer = overpassServer;
062        this.overpassQuery = overpassQuery.trim();
063    }
064
065    @Override
066    protected String getBaseUrl() {
067        return overpassServer;
068    }
069
070    @Override
071    protected String getRequestForBbox(double lon1, double lat1, double lon2, double lat2) {
072        if (overpassQuery.isEmpty())
073            return super.getRequestForBbox(lon1, lat1, lon2, lat2);
074        else {
075            final String query = this.overpassQuery.replace("{{bbox}}", lat1 + "," + lon1 + "," + lat2 + "," + lon2);
076            final String expandedOverpassQuery = expandExtendedQueries(query);
077            return "interpreter?data=" + Utils.encodeUrl(expandedOverpassQuery);
078        }
079    }
080
081    /**
082     * Evaluates some features of overpass turbo extended query syntax.
083     * See https://wiki.openstreetmap.org/wiki/Overpass_turbo/Extended_Overpass_Turbo_Queries
084     * @param query unexpanded query
085     * @return expanded query
086     */
087    static String expandExtendedQueries(String query) {
088        final StringBuffer sb = new StringBuffer();
089        final Matcher matcher = Pattern.compile("\\{\\{(geocodeArea):([^}]+)\\}\\}").matcher(query);
090        while (matcher.find()) {
091            try {
092                switch (matcher.group(1)) {
093                    case "geocodeArea":
094                        matcher.appendReplacement(sb, geocodeArea(matcher.group(2)));
095                        break;
096                    default:
097                        Main.warn("Unsupported syntax: " + matcher.group(1));
098                }
099            } catch (UncheckedParseException ex) {
100                final String msg = tr("Failed to evaluate {0}", matcher.group());
101                Main.warn(ex, msg);
102                matcher.appendReplacement(sb, "// " + msg + "\n");
103            }
104        }
105        matcher.appendTail(sb);
106        return sb.toString();
107    }
108
109    private static String geocodeArea(String area) {
110        // Offsets defined in https://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL#By_element_id
111        final EnumMap<OsmPrimitiveType, Long> idOffset = new EnumMap<>(OsmPrimitiveType.class);
112        idOffset.put(OsmPrimitiveType.NODE, 0L);
113        idOffset.put(OsmPrimitiveType.WAY, 2_400_000_000L);
114        idOffset.put(OsmPrimitiveType.RELATION, 3_600_000_000L);
115        try {
116            final List<NameFinder.SearchResult> results = NameFinder.queryNominatim(area);
117            final PrimitiveId osmId = results.iterator().next().getOsmId();
118            return String.format("area(%d)", osmId.getUniqueId() + idOffset.get(osmId.getType()));
119        } catch (IOException | NoSuchElementException ex) {
120            throw new UncheckedParseException(ex);
121        }
122    }
123
124    @Override
125    protected InputStream getInputStreamRaw(String urlStr, ProgressMonitor progressMonitor, String reason,
126                                            boolean uncompressAccordingToContentDisposition) throws OsmTransferException {
127        try {
128            return super.getInputStreamRaw(urlStr, progressMonitor, reason, uncompressAccordingToContentDisposition);
129        } catch (OsmApiException ex) {
130            final String errorIndicator = "Error</strong>: ";
131            if (ex.getMessage() != null && ex.getMessage().contains(errorIndicator)) {
132                final String errorPlusRest = ex.getMessage().split(errorIndicator)[1];
133                if (errorPlusRest != null) {
134                    final String error = errorPlusRest.split("</")[0];
135                    ex.setErrorHeader(error);
136                }
137            }
138            throw ex;
139        }
140    }
141
142    @Override
143    protected void adaptRequest(HttpClient request) {
144        // see https://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL#timeout
145        final Matcher timeoutMatcher = Pattern.compile("\\[timeout:(\\d+)\\]").matcher(overpassQuery);
146        final int timeout;
147        if (timeoutMatcher.find()) {
148            timeout = (int) TimeUnit.SECONDS.toMillis(Integer.parseInt(timeoutMatcher.group(1)));
149        } else {
150            timeout = (int) TimeUnit.MINUTES.toMillis(3);
151        }
152        request.setConnectTimeout(timeout);
153        request.setReadTimeout(timeout);
154    }
155
156    @Override
157    protected String getTaskName() {
158        return tr("Contacting Server...");
159    }
160
161    @Override
162    protected DataSet parseDataSet(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException {
163        return new OverpassOsmReader().doParseDataSet(source, progressMonitor);
164    }
165
166    @Override
167    public DataSet parseOsm(ProgressMonitor progressMonitor) throws OsmTransferException {
168
169        DataSet ds = super.parseOsm(progressMonitor);
170
171        // add bounds if necessary (note that Overpass API does not return bounds in the response XML)
172        if (ds != null && ds.dataSources.isEmpty() && overpassQuery.contains("{{bbox}}")) {
173            if (crosses180th) {
174                Bounds bounds = new Bounds(lat1, lon1, lat2, 180.0);
175                DataSource src = new DataSource(bounds, getBaseUrl());
176                ds.dataSources.add(src);
177
178                bounds = new Bounds(lat1, -180.0, lat2, lon2);
179                src = new DataSource(bounds, getBaseUrl());
180                ds.dataSources.add(src);
181            } else {
182                Bounds bounds = new Bounds(lat1, lon1, lat2, lon2);
183                DataSource src = new DataSource(bounds, getBaseUrl());
184                ds.dataSources.add(src);
185            }
186        }
187
188        return ds;
189    }
190}