001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions.downloadtasks; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.io.IOException; 007import java.net.URL; 008import java.util.Optional; 009import java.util.concurrent.Future; 010import java.util.regex.Matcher; 011import java.util.regex.Pattern; 012import java.util.stream.Stream; 013 014import org.openstreetmap.josm.Main; 015import org.openstreetmap.josm.data.Bounds; 016import org.openstreetmap.josm.data.Bounds.ParseMethod; 017import org.openstreetmap.josm.data.ViewportData; 018import org.openstreetmap.josm.data.gpx.GpxData; 019import org.openstreetmap.josm.gui.PleaseWaitRunnable; 020import org.openstreetmap.josm.gui.layer.GpxLayer; 021import org.openstreetmap.josm.gui.layer.Layer; 022import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer; 023import org.openstreetmap.josm.gui.progress.ProgressMonitor; 024import org.openstreetmap.josm.gui.progress.ProgressTaskId; 025import org.openstreetmap.josm.gui.progress.ProgressTaskIds; 026import org.openstreetmap.josm.io.BoundingBoxDownloader; 027import org.openstreetmap.josm.io.GpxImporter; 028import org.openstreetmap.josm.io.GpxImporter.GpxImporterData; 029import org.openstreetmap.josm.io.OsmServerLocationReader; 030import org.openstreetmap.josm.io.OsmServerReader; 031import org.openstreetmap.josm.io.OsmTransferException; 032import org.openstreetmap.josm.tools.CheckParameterUtil; 033import org.xml.sax.SAXException; 034 035/** 036 * Task allowing to download GPS data. 037 */ 038public class DownloadGpsTask extends AbstractDownloadTask<GpxData> { 039 040 private DownloadTask downloadTask; 041 042 private static final String PATTERN_TRACE_ID = "https?://.*(osm|openstreetmap).org/trace/\\p{Digit}+/data"; 043 private static final String PATTERN_USER_TRACE_ID = "https?://.*(osm|openstreetmap).org/user/[^/]+/traces/(\\p{Digit}+)"; 044 private static final String PATTERN_EDIT_TRACE_ID = "https?://.*(osm|openstreetmap).org/edit/?\\?gpx=(\\p{Digit}+)(#.*)?"; 045 046 private static final String PATTERN_TRACKPOINTS_BBOX = "https?://.*/api/0.6/trackpoints\\?bbox=.*,.*,.*,.*"; 047 048 private static final String PATTERN_EXTERNAL_GPX_SCRIPT = "https?://.*exportgpx.*"; 049 private static final String PATTERN_EXTERNAL_GPX_FILE = "https?://.*/(.*\\.gpx)"; 050 051 protected String newLayerName; 052 053 @Override 054 public String[] getPatterns() { 055 return new String[] { 056 PATTERN_EXTERNAL_GPX_FILE, PATTERN_EXTERNAL_GPX_SCRIPT, 057 PATTERN_TRACE_ID, PATTERN_USER_TRACE_ID, PATTERN_EDIT_TRACE_ID, 058 PATTERN_TRACKPOINTS_BBOX, 059 }; 060 } 061 062 @Override 063 public String getTitle() { 064 return tr("Download GPS"); 065 } 066 067 @Override 068 public Future<?> download(boolean newLayer, Bounds downloadArea, ProgressMonitor progressMonitor) { 069 downloadTask = new DownloadTask(newLayer, 070 new BoundingBoxDownloader(downloadArea), progressMonitor); 071 // We need submit instead of execute so we can wait for it to finish and get the error 072 // message if necessary. If no one calls getErrorMessage() it just behaves like execute. 073 return Main.worker.submit(downloadTask); 074 } 075 076 @Override 077 public Future<?> loadUrl(boolean newLayer, String url, ProgressMonitor progressMonitor) { 078 CheckParameterUtil.ensureParameterNotNull(url, "url"); 079 final Optional<String> mappedUrl = Stream.of(PATTERN_USER_TRACE_ID, PATTERN_EDIT_TRACE_ID) 080 .map(p -> Pattern.compile(p).matcher(url)) 081 .filter(Matcher::matches) 082 .map(m -> "https://www.openstreetmap.org/trace/" + m.group(2) + "/data") 083 .findFirst(); 084 if (mappedUrl.isPresent()) { 085 return loadUrl(newLayer, mappedUrl.get(), progressMonitor); 086 } 087 if (url.matches(PATTERN_TRACE_ID) 088 || url.matches(PATTERN_EXTERNAL_GPX_SCRIPT) 089 || url.matches(PATTERN_EXTERNAL_GPX_FILE)) { 090 downloadTask = new DownloadTask(newLayer, 091 new OsmServerLocationReader(url), progressMonitor); 092 // Extract .gpx filename from URL to set the new layer name 093 Matcher matcher = Pattern.compile(PATTERN_EXTERNAL_GPX_FILE).matcher(url); 094 newLayerName = matcher.matches() ? matcher.group(1) : null; 095 // We need submit instead of execute so we can wait for it to finish and get the error 096 // message if necessary. If no one calls getErrorMessage() it just behaves like execute. 097 return Main.worker.submit(downloadTask); 098 099 } else if (url.matches(PATTERN_TRACKPOINTS_BBOX)) { 100 String[] table = url.split("\\?|=|&"); 101 for (int i = 0; i < table.length; i++) { 102 if ("bbox".equals(table[i]) && i < table.length-1) 103 return download(newLayer, new Bounds(table[i+1], ",", ParseMethod.LEFT_BOTTOM_RIGHT_TOP), progressMonitor); 104 } 105 } 106 return null; 107 } 108 109 @Override 110 public void cancel() { 111 if (downloadTask != null) { 112 downloadTask.cancel(); 113 } 114 } 115 116 class DownloadTask extends PleaseWaitRunnable { 117 private final OsmServerReader reader; 118 private GpxData rawData; 119 private final boolean newLayer; 120 121 DownloadTask(boolean newLayer, OsmServerReader reader, ProgressMonitor progressMonitor) { 122 super(tr("Downloading GPS data"), progressMonitor, false); 123 this.reader = reader; 124 this.newLayer = newLayer; 125 } 126 127 @Override 128 public void realRun() throws IOException, SAXException, OsmTransferException { 129 try { 130 if (isCanceled()) 131 return; 132 ProgressMonitor subMonitor = progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false); 133 rawData = reader.parseRawGps(subMonitor); 134 } catch (OsmTransferException e) { 135 if (isCanceled()) 136 return; 137 rememberException(e); 138 } 139 } 140 141 @Override 142 protected void finish() { 143 rememberDownloadedData(rawData); 144 if (rawData == null) 145 return; 146 String name = newLayerName != null ? newLayerName : tr("Downloaded GPX Data"); 147 148 GpxImporterData layers = GpxImporter.loadLayers(rawData, reader.isGpxParsedProperly(), name, 149 tr("Markers from {0}", name)); 150 151 GpxLayer gpxLayer = addOrMergeLayer(layers.getGpxLayer(), findGpxMergeLayer()); 152 addOrMergeLayer(layers.getMarkerLayer(), findMarkerMergeLayer(gpxLayer)); 153 154 layers.getPostLayerTask().run(); 155 } 156 157 private <L extends Layer> L addOrMergeLayer(L layer, L mergeLayer) { 158 if (layer == null) return null; 159 if (newLayer || mergeLayer == null) { 160 Main.getLayerManager().addLayer(layer); 161 return layer; 162 } else { 163 mergeLayer.mergeFrom(layer); 164 mergeLayer.invalidate(); 165 if (Main.map != null) { 166 Main.map.mapView.scheduleZoomTo(new ViewportData(layer.getViewProjectionBounds())); 167 } 168 return mergeLayer; 169 } 170 } 171 172 private GpxLayer findGpxMergeLayer() { 173 boolean merge = Main.pref.getBoolean("download.gps.mergeWithLocal", false); 174 Layer active = Main.getLayerManager().getActiveLayer(); 175 if (active instanceof GpxLayer && (merge || ((GpxLayer) active).data.fromServer)) 176 return (GpxLayer) active; 177 for (GpxLayer l : Main.getLayerManager().getLayersOfType(GpxLayer.class)) { 178 if (merge || l.data.fromServer) 179 return l; 180 } 181 return null; 182 } 183 184 private MarkerLayer findMarkerMergeLayer(GpxLayer fromLayer) { 185 for (MarkerLayer l : Main.getLayerManager().getLayersOfType(MarkerLayer.class)) { 186 if (fromLayer != null && l.fromLayer == fromLayer) 187 return l; 188 } 189 return null; 190 } 191 192 @Override 193 protected void cancel() { 194 setCanceled(true); 195 if (reader != null) { 196 reader.cancel(); 197 } 198 } 199 200 @Override 201 public ProgressTaskId canRunInBackground() { 202 return ProgressTaskIds.DOWNLOAD_GPS; 203 } 204 } 205 206 @Override 207 public String getConfirmationMessage(URL url) { 208 // TODO 209 return null; 210 } 211 212 @Override 213 public boolean isSafeForRemotecontrolRequests() { 214 return true; 215 } 216 217 /** 218 * Determines if the given URL denotes an OSM gpx-related API call. 219 * @param url The url to check 220 * @return true if the url matches "Trace ID" API call or "Trackpoints bbox" API call, false otherwise 221 * @see GpxData#fromServer 222 * @since 5745 223 */ 224 public static final boolean isFromServer(String url) { 225 return url != null && (url.matches(PATTERN_TRACE_ID) || url.matches(PATTERN_TRACKPOINTS_BBOX)); 226 } 227}