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.Objects;
009import java.util.Optional;
010import java.util.concurrent.Future;
011import java.util.regex.Matcher;
012import java.util.stream.Stream;
013
014import org.openstreetmap.josm.data.Bounds;
015import org.openstreetmap.josm.data.Bounds.ParseMethod;
016import org.openstreetmap.josm.data.ProjectionBounds;
017import org.openstreetmap.josm.data.gpx.GpxConstants;
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.OsmServerReader;
032import org.openstreetmap.josm.io.OsmTransferException;
033import org.openstreetmap.josm.io.UrlPatterns.GpxUrlPattern;
034import org.openstreetmap.josm.spi.preferences.Config;
035import org.openstreetmap.josm.tools.Utils;
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 url;
047
048    @Override
049    public String[] getPatterns() {
050        return patterns(GpxUrlPattern.class);
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        this.url = Objects.requireNonNull(url);
070        final Optional<String> mappedUrl = Stream.of(GpxUrlPattern.USER_TRACE_ID, GpxUrlPattern.EDIT_TRACE_ID)
071                .map(p -> p.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 -> p.matches(url))) {
081            downloadTask = new DownloadTask(settings,
082                    new OsmServerLocationReader(url), progressMonitor);
083            // We need submit instead of execute so we can wait for it to finish and get the error
084            // message if necessary. If no one calls getErrorMessage() it just behaves like execute.
085            return MainApplication.worker.submit(downloadTask);
086
087        } else if (GpxUrlPattern.TRACKPOINTS_BBOX.matches(url)) {
088            String[] table = url.split("\\?|=|&");
089            for (int i = 0; i < table.length; i++) {
090                if ("bbox".equals(table[i]) && i < table.length-1)
091                    return download(settings, new Bounds(table[i+1], ",", ParseMethod.LEFT_BOTTOM_RIGHT_TOP), progressMonitor);
092            }
093        }
094        return null;
095    }
096
097    @Override
098    public void cancel() {
099        if (downloadTask != null) {
100            downloadTask.cancel();
101        }
102    }
103
104    @Override
105    public ProjectionBounds getDownloadProjectionBounds() {
106        return gpxLayer != null ? gpxLayer.getViewProjectionBounds() : null;
107    }
108
109    class DownloadTask extends PleaseWaitRunnable {
110        private final OsmServerReader reader;
111        private GpxData rawData;
112        private final boolean newLayer;
113
114        DownloadTask(DownloadParams settings, OsmServerReader reader, ProgressMonitor progressMonitor) {
115            super(tr("Downloading GPS data"), progressMonitor, false);
116            this.reader = reader;
117            this.newLayer = settings.isNewLayer();
118        }
119
120        @Override
121        public void realRun() throws IOException, SAXException, OsmTransferException {
122            try {
123                if (isCanceled())
124                    return;
125                rawData = reader.parseRawGps(progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
126            } catch (OsmTransferException e) {
127                if (isCanceled())
128                    return;
129                rememberException(e);
130            }
131        }
132
133        @Override
134        protected void finish() {
135            rememberDownloadedData(rawData);
136            if (rawData == null)
137                return;
138            String name = getLayerName();
139
140            GpxImporterData layers = GpxImporter.loadLayers(rawData, reader.isGpxParsedProperly(), name,
141                    tr("Markers from {0}", name));
142
143            gpxLayer = layers.getGpxLayer();
144            addOrMergeLayer(gpxLayer, findGpxMergeLayer());
145            addOrMergeLayer(layers.getMarkerLayer(), findMarkerMergeLayer(gpxLayer));
146
147            layers.getPostLayerTask().run();
148        }
149
150        private String getLayerName() {
151            // Extract .gpx filename from URL to set the new layer name
152            final Matcher matcher = url != null ? GpxUrlPattern.EXTERNAL_GPX_FILE.matcher(url) : null;
153            final String newLayerName = matcher != null && matcher.matches() ? matcher.group(1) : null;
154            final String metadataName = rawData != null ? rawData.getString(GpxConstants.META_NAME) : null;
155            final String defaultName = tr("Downloaded GPX Data");
156
157            if (Config.getPref().getBoolean("gpx.prefermetadataname", false)) {
158                return Utils.firstNotEmptyString(defaultName, metadataName, newLayerName);
159            } else {
160                return Utils.firstNotEmptyString(defaultName, newLayerName, metadataName);
161            }
162        }
163
164        private <L extends Layer> L addOrMergeLayer(L layer, L mergeLayer) {
165            if (layer == null) return null;
166            if (newLayer || mergeLayer == null) {
167                MainApplication.getLayerManager().addLayer(layer, zoomAfterDownload);
168                return layer;
169            } else {
170                mergeLayer.mergeFrom(layer);
171                mergeLayer.invalidate();
172                return mergeLayer;
173            }
174        }
175
176        private GpxLayer findGpxMergeLayer() {
177            boolean merge = Config.getPref().getBoolean("download.gps.mergeWithLocal", false);
178            Layer active = MainApplication.getLayerManager().getActiveLayer();
179            if (active instanceof GpxLayer && (merge || ((GpxLayer) active).data.fromServer))
180                return (GpxLayer) active;
181            for (GpxLayer l : MainApplication.getLayerManager().getLayersOfType(GpxLayer.class)) {
182                if (merge || l.data.fromServer)
183                    return l;
184            }
185            return null;
186        }
187
188        private MarkerLayer findMarkerMergeLayer(GpxLayer fromLayer) {
189            for (MarkerLayer l : MainApplication.getLayerManager().getLayersOfType(MarkerLayer.class)) {
190                if (fromLayer != null && l.fromLayer == fromLayer)
191                    return l;
192            }
193            return null;
194        }
195
196        @Override
197        protected void cancel() {
198            setCanceled(true);
199            if (reader != null) {
200                reader.cancel();
201            }
202        }
203
204        @Override
205        public ProgressTaskId canRunInBackground() {
206            return ProgressTaskIds.DOWNLOAD_GPS;
207        }
208    }
209
210    @Override
211    public String getConfirmationMessage(URL url) {
212        // TODO
213        return null;
214    }
215
216    @Override
217    public boolean isSafeForRemotecontrolRequests() {
218        return true;
219    }
220}