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(DownloadParams settings, Bounds downloadArea, ProgressMonitor progressMonitor) {
062        downloadTask = new DownloadTask(settings,
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(DownloadParams settings, 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(settings, 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(settings,
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(settings, 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(DownloadParams settings, OsmServerReader reader, ProgressMonitor progressMonitor) {
120            super(tr("Downloading GPS data"), progressMonitor, false);
121            this.reader = reader;
122            this.newLayer = settings.isNewLayer();
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}