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