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}