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