// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.io;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.io.IOException;
import java.io.InputStream;
import java.util.EnumMap;
import java.util.NoSuchElementException;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.Bounds;
import org.openstreetmap.josm.data.DataSource;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
import org.openstreetmap.josm.data.osm.PrimitiveId;
import org.openstreetmap.josm.gui.progress.ProgressMonitor;
import org.openstreetmap.josm.tools.HttpClient;
import org.openstreetmap.josm.tools.UncheckedParseException;
import org.openstreetmap.josm.tools.Utils;
/**
* Read content from an Overpass server.
*
* @since 8744
*/
public class OverpassDownloadReader extends BoundingBoxDownloader {
static final class OverpassOsmReader extends OsmReader {
@Override
protected void parseUnknown(boolean printWarning) throws XMLStreamException {
if ("remark".equals(parser.getLocalName()) && parser.getEventType() == XMLStreamConstants.START_ELEMENT) {
final String text = parser.getElementText();
if (text.contains("runtime error")) {
throw new XMLStreamException(text);
}
}
super.parseUnknown(printWarning);
}
}
final String overpassServer;
final String overpassQuery;
/**
* Constructs a new {@code OverpassDownloadReader}.
*
* @param downloadArea The area to download
* @param overpassServer The Overpass server to use
* @param overpassQuery The Overpass query
*/
public OverpassDownloadReader(Bounds downloadArea, String overpassServer, String overpassQuery) {
super(downloadArea);
this.overpassServer = overpassServer;
this.overpassQuery = overpassQuery.trim();
}
@Override
protected String getBaseUrl() {
return overpassServer;
}
@Override
protected String getRequestForBbox(double lon1, double lat1, double lon2, double lat2) {
if (overpassQuery.isEmpty())
return super.getRequestForBbox(lon1, lat1, lon2, lat2);
else {
final String query = this.overpassQuery.replace("{{bbox}}", lat1 + "," + lon1 + "," + lat2 + "," + lon2);
final String expandedOverpassQuery = expandExtendedQueries(query);
return "interpreter?data=" + Utils.encodeUrl(expandedOverpassQuery);
}
}
/**
* Evaluates some features of overpass turbo extended query syntax.
* See https://wiki.openstreetmap.org/wiki/Overpass_turbo/Extended_Overpass_Turbo_Queries
* @param query unexpanded query
* @return expanded query
*/
static String expandExtendedQueries(String query) {
final StringBuffer sb = new StringBuffer();
final Matcher matcher = Pattern.compile("\\{\\{(geocodeArea):([^}]+)\\}\\}").matcher(query);
while (matcher.find()) {
try {
switch (matcher.group(1)) {
case "geocodeArea":
matcher.appendReplacement(sb, geocodeArea(matcher.group(2)));
break;
default:
Main.warn("Unsupported syntax: " + matcher.group(1));
}
} catch (UncheckedParseException ex) {
final String msg = tr("Failed to evaluate {0}", matcher.group());
Main.warn(ex, msg);
matcher.appendReplacement(sb, "// " + msg + "\n");
}
}
matcher.appendTail(sb);
return sb.toString();
}
private static String geocodeArea(String area) {
// Offsets defined in https://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL#By_element_id
final EnumMap<OsmPrimitiveType, Long> idOffset = new EnumMap<>(OsmPrimitiveType.class);
idOffset.put(OsmPrimitiveType.NODE, 0L);
idOffset.put(OsmPrimitiveType.WAY, 2_400_000_000L);
idOffset.put(OsmPrimitiveType.RELATION, 3_600_000_000L);
try {
final PrimitiveId osmId = NameFinder.queryNominatim(area).stream().filter(
x -> !OsmPrimitiveType.NODE.equals(x.getOsmId().getType())).iterator().next().getOsmId();
return String.format("area(%d)", osmId.getUniqueId() + idOffset.get(osmId.getType()));
} catch (IOException | NoSuchElementException | IndexOutOfBoundsException ex) {
throw new UncheckedParseException(ex);
}
}
@Override
protected InputStream getInputStreamRaw(String urlStr, ProgressMonitor progressMonitor, String reason,
boolean uncompressAccordingToContentDisposition) throws OsmTransferException {
try {
return super.getInputStreamRaw(urlStr, progressMonitor, reason, uncompressAccordingToContentDisposition);
} catch (OsmApiException ex) {
final String errorIndicator = "Error</strong>: ";
if (ex.getMessage() != null && ex.getMessage().contains(errorIndicator)) {
final String errorPlusRest = ex.getMessage().split(errorIndicator)[1];
if (errorPlusRest != null) {
final String error = errorPlusRest.split("</")[0];
ex.setErrorHeader(error);
}
}
throw ex;
}
}
@Override
protected void adaptRequest(HttpClient request) {
// see https://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL#timeout
final Matcher timeoutMatcher = Pattern.compile("\\[timeout:(\\d+)\\]").matcher(overpassQuery);
final int timeout;
if (timeoutMatcher.find()) {
timeout = (int) TimeUnit.SECONDS.toMillis(Integer.parseInt(timeoutMatcher.group(1)));
} else {
timeout = (int) TimeUnit.MINUTES.toMillis(3);
}
request.setConnectTimeout(timeout);
request.setReadTimeout(timeout);
}
@Override
protected String getTaskName() {
return tr("Contacting Server...");
}
@Override
protected DataSet parseDataSet(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException {
return new OverpassOsmReader().doParseDataSet(source, progressMonitor);
}
@Override
public DataSet parseOsm(ProgressMonitor progressMonitor) throws OsmTransferException {
DataSet ds = super.parseOsm(progressMonitor);
// add bounds if necessary (note that Overpass API does not return bounds in the response XML)
if (ds != null && ds.getDataSources().isEmpty() && overpassQuery.contains("{{bbox}}")) {
if (crosses180th) {
Bounds bounds = new Bounds(lat1, lon1, lat2, 180.0);
DataSource src = new DataSource(bounds, getBaseUrl());
ds.addDataSource(src);
bounds = new Bounds(lat1, -180.0, lat2, lon2);
src = new DataSource(bounds, getBaseUrl());
ds.addDataSource(src);
} else {
Bounds bounds = new Bounds(lat1, lon1, lat2, lon2);
DataSource src = new DataSource(bounds, getBaseUrl());
ds.addDataSource(src);
}
}
return ds;
}
}