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