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.InputStream;
007import java.util.regex.Matcher;
008import java.util.regex.Pattern;
009
010import javax.xml.stream.XMLStreamConstants;
011import javax.xml.stream.XMLStreamException;
012
013import org.openstreetmap.josm.data.Bounds;
014import org.openstreetmap.josm.data.DataSource;
015import org.openstreetmap.josm.data.osm.DataSet;
016import org.openstreetmap.josm.gui.progress.ProgressMonitor;
017import org.openstreetmap.josm.tools.HttpClient;
018import org.openstreetmap.josm.tools.Utils;
019
020/**
021 * Read content from an Overpass server.
022 *
023 * @since 8744
024 */
025public class OverpassDownloadReader extends BoundingBoxDownloader {
026
027    final String overpassServer;
028    final String overpassQuery;
029
030    /**
031     * Constructs a new {@code OverpassDownloadReader}.
032     *
033     * @param downloadArea   The area to download
034     * @param overpassServer The Overpass server to use
035     * @param overpassQuery  The Overpass query
036     */
037    public OverpassDownloadReader(Bounds downloadArea, String overpassServer, String overpassQuery) {
038        super(downloadArea);
039        this.overpassServer = overpassServer;
040        this.overpassQuery = overpassQuery.trim();
041    }
042
043    @Override
044    protected String getBaseUrl() {
045        return overpassServer;
046    }
047
048    @Override
049    protected String getRequestForBbox(double lon1, double lat1, double lon2, double lat2) {
050        if (overpassQuery.isEmpty())
051            return super.getRequestForBbox(lon1, lat1, lon2, lat2);
052        else {
053            String realQuery = completeOverpassQuery(overpassQuery);
054            return "interpreter?data=" + Utils.encodeUrl(realQuery)
055                    + "&bbox=" + lon1 + ',' + lat1 + ',' + lon2 + ',' + lat2;
056        }
057    }
058
059    private static String completeOverpassQuery(String query) {
060        int firstColon = query.indexOf(';');
061        if (firstColon == -1) {
062            return "[bbox];" + query;
063        }
064        int bboxPos = query.indexOf("[bbox");
065        if (bboxPos > -1 && bboxPos < firstColon) {
066            return query;
067        }
068
069        int bracketCount = 0;
070        int pos = 0;
071        for (; pos < firstColon; ++pos) {
072            if (query.charAt(pos) == '[')
073                ++bracketCount;
074            else if (query.charAt(pos) == ']')
075                --bracketCount;
076            else if (bracketCount == 0) {
077                if (!Character.isWhitespace(query.charAt(pos)))
078                    break;
079            }
080        }
081
082        if (pos < firstColon) {
083            // We start with a statement, not with declarations
084            return "[bbox];" + query;
085        }
086
087        // We start with declarations. Add just one more declaration in this case.
088        return "[bbox]" + query;
089    }
090
091    @Override
092    protected InputStream getInputStreamRaw(String urlStr, ProgressMonitor progressMonitor, String reason,
093                                            boolean uncompressAccordingToContentDisposition) throws OsmTransferException {
094        try {
095            return super.getInputStreamRaw(urlStr, progressMonitor, reason, uncompressAccordingToContentDisposition);
096        } catch (OsmApiException ex) {
097            final String errorIndicator = "Error</strong>: ";
098            if (ex.getMessage() != null && ex.getMessage().contains(errorIndicator)) {
099                final String errorPlusRest = ex.getMessage().split(errorIndicator)[1];
100                if (errorPlusRest != null) {
101                    final String error = errorPlusRest.split("</")[0];
102                    ex.setErrorHeader(error);
103                }
104            }
105            throw ex;
106        }
107    }
108
109    @Override
110    protected void adaptRequest(HttpClient request) {
111        // see https://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL#timeout
112        final Matcher timeoutMatcher = Pattern.compile("\\[timeout:(\\d+)\\]").matcher(overpassQuery);
113        final int timeout;
114        if (timeoutMatcher.find()) {
115            timeout = 1000 * Integer.parseInt(timeoutMatcher.group(1));
116        } else {
117            timeout = 180_000;
118        }
119        request.setConnectTimeout(timeout);
120        request.setReadTimeout(timeout);
121    }
122
123    @Override
124    protected String getTaskName() {
125        return tr("Contacting Server...");
126    }
127
128    @Override
129    protected DataSet parseDataSet(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException {
130        return new OsmReader() {
131            @Override
132            protected void parseUnknown(boolean printWarning) throws XMLStreamException {
133                if ("remark".equals(parser.getLocalName())) {
134                    if (parser.getEventType() == XMLStreamConstants.START_ELEMENT) {
135                        final String text = parser.getElementText();
136                        if (text.contains("runtime error")) {
137                            throw new XMLStreamException(text);
138                        }
139                    }
140                }
141                super.parseUnknown(printWarning);
142            }
143        }.doParseDataSet(source, progressMonitor);
144    }
145
146    @Override
147    public DataSet parseOsm(ProgressMonitor progressMonitor) throws OsmTransferException {
148
149        DataSet ds = super.parseOsm(progressMonitor);
150
151        // add bounds if necessary (note that Overpass API does not return bounds in the response XML)
152        if (ds != null && ds.dataSources.isEmpty()) {
153            if (crosses180th) {
154                Bounds bounds = new Bounds(lat1, lon1, lat2, 180.0);
155                DataSource src = new DataSource(bounds, getBaseUrl());
156                ds.dataSources.add(src);
157
158                bounds = new Bounds(lat1, -180.0, lat2, lon2);
159                src = new DataSource(bounds, getBaseUrl());
160                ds.dataSources.add(src);
161            } else {
162                Bounds bounds = new Bounds(lat1, lon1, lat2, lon2);
163                DataSource src = new DataSource(bounds, getBaseUrl());
164                ds.dataSources.add(src);
165            }
166        }
167
168        return ds;
169    }
170}