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