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