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