// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.actions.downloadtasks; import static org.openstreetmap.josm.tools.I18n.tr; import java.io.IOException; import java.net.URL; import java.util.Optional; import java.util.concurrent.Future; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.Bounds; import org.openstreetmap.josm.data.Bounds.ParseMethod; import org.openstreetmap.josm.data.ProjectionBounds; import org.openstreetmap.josm.data.ViewportData; import org.openstreetmap.josm.data.gpx.GpxData; import org.openstreetmap.josm.gui.PleaseWaitRunnable; import org.openstreetmap.josm.gui.layer.GpxLayer; import org.openstreetmap.josm.gui.layer.Layer; import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer; import org.openstreetmap.josm.gui.progress.ProgressMonitor; import org.openstreetmap.josm.gui.progress.ProgressTaskId; import org.openstreetmap.josm.gui.progress.ProgressTaskIds; import org.openstreetmap.josm.io.BoundingBoxDownloader; import org.openstreetmap.josm.io.GpxImporter; import org.openstreetmap.josm.io.GpxImporter.GpxImporterData; import org.openstreetmap.josm.io.OsmServerLocationReader; import org.openstreetmap.josm.io.OsmServerReader; import org.openstreetmap.josm.io.OsmTransferException; import org.openstreetmap.josm.tools.CheckParameterUtil; import org.xml.sax.SAXException; /** * Task allowing to download GPS data. */ public class DownloadGpsTask extends AbstractDownloadTask<GpxData> { private DownloadTask downloadTask; private GpxLayer gpxLayer; private static final String PATTERN_TRACE_ID = "https?://.*(osm|openstreetmap).org/trace/\\p{Digit}+/data"; private static final String PATTERN_USER_TRACE_ID = "https?://.*(osm|openstreetmap).org/user/[^/]+/traces/(\\p{Digit}+)"; private static final String PATTERN_EDIT_TRACE_ID = "https?://.*(osm|openstreetmap).org/edit/?\\?gpx=(\\p{Digit}+)(#.*)?"; private static final String PATTERN_TRACKPOINTS_BBOX = "https?://.*/api/0.6/trackpoints\\?bbox=.*,.*,.*,.*"; private static final String PATTERN_EXTERNAL_GPX_SCRIPT = "https?://.*exportgpx.*"; private static final String PATTERN_EXTERNAL_GPX_FILE = "https?://.*/(.*\\.gpx)"; protected String newLayerName; @Override public String[] getPatterns() { return new String[] { PATTERN_EXTERNAL_GPX_FILE, PATTERN_EXTERNAL_GPX_SCRIPT, PATTERN_TRACE_ID, PATTERN_USER_TRACE_ID, PATTERN_EDIT_TRACE_ID, PATTERN_TRACKPOINTS_BBOX, }; } @Override public String getTitle() { return tr("Download GPS"); } @Override public Future<?> download(boolean newLayer, Bounds downloadArea, ProgressMonitor progressMonitor) { downloadTask = new DownloadTask(newLayer, new BoundingBoxDownloader(downloadArea), progressMonitor); // We need submit instead of execute so we can wait for it to finish and get the error // message if necessary. If no one calls getErrorMessage() it just behaves like execute. return Main.worker.submit(downloadTask); } @Override public Future<?> loadUrl(boolean newLayer, String url, ProgressMonitor progressMonitor) { CheckParameterUtil.ensureParameterNotNull(url, "url"); final Optional<String> mappedUrl = Stream.of(PATTERN_USER_TRACE_ID, PATTERN_EDIT_TRACE_ID) .map(p -> Pattern.compile(p).matcher(url)) .filter(Matcher::matches) .map(m -> "https://www.openstreetmap.org/trace/" + m.group(2) + "/data") .findFirst(); if (mappedUrl.isPresent()) { return loadUrl(newLayer, mappedUrl.get(), progressMonitor); } if (url.matches(PATTERN_TRACE_ID) || url.matches(PATTERN_EXTERNAL_GPX_SCRIPT) || url.matches(PATTERN_EXTERNAL_GPX_FILE)) { downloadTask = new DownloadTask(newLayer, new OsmServerLocationReader(url), progressMonitor); // Extract .gpx filename from URL to set the new layer name Matcher matcher = Pattern.compile(PATTERN_EXTERNAL_GPX_FILE).matcher(url); newLayerName = matcher.matches() ? matcher.group(1) : null; // We need submit instead of execute so we can wait for it to finish and get the error // message if necessary. If no one calls getErrorMessage() it just behaves like execute. return Main.worker.submit(downloadTask); } else if (url.matches(PATTERN_TRACKPOINTS_BBOX)) { String[] table = url.split("\\?|=|&"); for (int i = 0; i < table.length; i++) { if ("bbox".equals(table[i]) && i < table.length-1) return download(newLayer, new Bounds(table[i+1], ",", ParseMethod.LEFT_BOTTOM_RIGHT_TOP), progressMonitor); } } return null; } @Override public void cancel() { if (downloadTask != null) { downloadTask.cancel(); } } @Override public ProjectionBounds getDownloadProjectionBounds() { return gpxLayer != null ? gpxLayer.getViewProjectionBounds() : null; } class DownloadTask extends PleaseWaitRunnable { private final OsmServerReader reader; private GpxData rawData; private final boolean newLayer; DownloadTask(boolean newLayer, OsmServerReader reader, ProgressMonitor progressMonitor) { super(tr("Downloading GPS data"), progressMonitor, false); this.reader = reader; this.newLayer = newLayer; } @Override public void realRun() throws IOException, SAXException, OsmTransferException { try { if (isCanceled()) return; rawData = reader.parseRawGps(progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)); } catch (OsmTransferException e) { if (isCanceled()) return; rememberException(e); } } @Override protected void finish() { rememberDownloadedData(rawData); if (rawData == null) return; String name = newLayerName != null ? newLayerName : tr("Downloaded GPX Data"); GpxImporterData layers = GpxImporter.loadLayers(rawData, reader.isGpxParsedProperly(), name, tr("Markers from {0}", name)); gpxLayer = layers.getGpxLayer(); addOrMergeLayer(gpxLayer, findGpxMergeLayer()); addOrMergeLayer(layers.getMarkerLayer(), findMarkerMergeLayer(gpxLayer)); layers.getPostLayerTask().run(); } private <L extends Layer> L addOrMergeLayer(L layer, L mergeLayer) { if (layer == null) return null; if (newLayer || mergeLayer == null) { Main.getLayerManager().addLayer(layer, zoomAfterDownload); return layer; } else { mergeLayer.mergeFrom(layer); mergeLayer.invalidate(); if (Main.map != null && zoomAfterDownload && layer instanceof GpxLayer) { Main.map.mapView.scheduleZoomTo(new ViewportData(layer.getViewProjectionBounds())); } return mergeLayer; } } private GpxLayer findGpxMergeLayer() { boolean merge = Main.pref.getBoolean("download.gps.mergeWithLocal", false); Layer active = Main.getLayerManager().getActiveLayer(); if (active instanceof GpxLayer && (merge || ((GpxLayer) active).data.fromServer)) return (GpxLayer) active; for (GpxLayer l : Main.getLayerManager().getLayersOfType(GpxLayer.class)) { if (merge || l.data.fromServer) return l; } return null; } private MarkerLayer findMarkerMergeLayer(GpxLayer fromLayer) { for (MarkerLayer l : Main.getLayerManager().getLayersOfType(MarkerLayer.class)) { if (fromLayer != null && l.fromLayer == fromLayer) return l; } return null; } @Override protected void cancel() { setCanceled(true); if (reader != null) { reader.cancel(); } } @Override public ProgressTaskId canRunInBackground() { return ProgressTaskIds.DOWNLOAD_GPS; } } @Override public String getConfirmationMessage(URL url) { // TODO return null; } @Override public boolean isSafeForRemotecontrolRequests() { return true; } /** * Determines if the given URL denotes an OSM gpx-related API call. * @param url The url to check * @return true if the url matches "Trace ID" API call or "Trackpoints bbox" API call, false otherwise * @see GpxData#fromServer * @since 5745 */ public static final boolean isFromServer(String url) { return url != null && (url.matches(PATTERN_TRACE_ID) || url.matches(PATTERN_TRACKPOINTS_BBOX)); } }