source: josm/trunk/src/org/openstreetmap/josm/actions/downloadtasks/DownloadOsmTask.java@ 12726

Last change on this file since 12726 was 12679, checked in by Don-vip, 7 years ago

see #15182 - make actions.downloadtasks.Download*Task depend on io.OsmServerLocationReader, not the opposite

  • Property svn:eol-style set to native
File size: 17.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions.downloadtasks;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.IOException;
7import java.net.URL;
8import java.util.ArrayList;
9import java.util.Arrays;
10import java.util.Collection;
11import java.util.Collections;
12import java.util.HashSet;
13import java.util.Optional;
14import java.util.Set;
15import java.util.concurrent.Future;
16import java.util.regex.Matcher;
17import java.util.regex.Pattern;
18
19import org.openstreetmap.josm.data.Bounds;
20import org.openstreetmap.josm.data.DataSource;
21import org.openstreetmap.josm.data.ProjectionBounds;
22import org.openstreetmap.josm.data.ViewportData;
23import org.openstreetmap.josm.data.coor.LatLon;
24import org.openstreetmap.josm.data.osm.DataSet;
25import org.openstreetmap.josm.data.osm.OsmPrimitive;
26import org.openstreetmap.josm.data.osm.Relation;
27import org.openstreetmap.josm.data.osm.Way;
28import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
29import org.openstreetmap.josm.gui.MainApplication;
30import org.openstreetmap.josm.gui.MapFrame;
31import org.openstreetmap.josm.gui.PleaseWaitRunnable;
32import org.openstreetmap.josm.gui.io.UpdatePrimitivesTask;
33import org.openstreetmap.josm.gui.layer.OsmDataLayer;
34import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
35import org.openstreetmap.josm.gui.progress.ProgressMonitor;
36import org.openstreetmap.josm.io.BoundingBoxDownloader;
37import org.openstreetmap.josm.io.OsmServerLocationReader;
38import org.openstreetmap.josm.io.OsmServerLocationReader.OsmUrlPattern;
39import org.openstreetmap.josm.io.OsmServerReader;
40import org.openstreetmap.josm.io.OsmTransferCanceledException;
41import org.openstreetmap.josm.io.OsmTransferException;
42import org.openstreetmap.josm.tools.Logging;
43import org.openstreetmap.josm.tools.Utils;
44import org.xml.sax.SAXException;
45
46/**
47 * Open the download dialog and download the data.
48 * Run in the worker thread.
49 */
50public class DownloadOsmTask extends AbstractDownloadTask<DataSet> {
51
52 protected Bounds currentBounds;
53 protected DownloadTask downloadTask;
54
55 protected String newLayerName;
56
57 /** This allows subclasses to ignore this warning */
58 protected boolean warnAboutEmptyArea = true;
59
60 @Override
61 public String[] getPatterns() {
62 if (this.getClass() == DownloadOsmTask.class) {
63 return Arrays.stream(OsmUrlPattern.values()).map(OsmUrlPattern::pattern).toArray(String[]::new);
64 } else {
65 return super.getPatterns();
66 }
67 }
68
69 @Override
70 public String getTitle() {
71 if (this.getClass() == DownloadOsmTask.class) {
72 return tr("Download OSM");
73 } else {
74 return super.getTitle();
75 }
76 }
77
78 @Override
79 public Future<?> download(boolean newLayer, Bounds downloadArea, ProgressMonitor progressMonitor) {
80 return download(new BoundingBoxDownloader(downloadArea), newLayer, downloadArea, progressMonitor);
81 }
82
83 /**
84 * Asynchronously launches the download task for a given bounding box.
85 *
86 * Set <code>progressMonitor</code> to null, if the task should create, open, and close a progress monitor.
87 * Set progressMonitor to {@link NullProgressMonitor#INSTANCE} if progress information is to
88 * be discarded.
89 *
90 * You can wait for the asynchronous download task to finish by synchronizing on the returned
91 * {@link Future}, but make sure not to freeze up JOSM. Example:
92 * <pre>
93 * Future&lt;?&gt; future = task.download(...);
94 * // DON'T run this on the Swing EDT or JOSM will freeze
95 * future.get(); // waits for the dowload task to complete
96 * </pre>
97 *
98 * The following example uses a pattern which is better suited if a task is launched from
99 * the Swing EDT:
100 * <pre>
101 * final Future&lt;?&gt; future = task.download(...);
102 * Runnable runAfterTask = new Runnable() {
103 * public void run() {
104 * // this is not strictly necessary because of the type of executor service
105 * // Main.worker is initialized with, but it doesn't harm either
106 * //
107 * future.get(); // wait for the download task to complete
108 * doSomethingAfterTheTaskCompleted();
109 * }
110 * }
111 * MainApplication.worker.submit(runAfterTask);
112 * </pre>
113 * @param reader the reader used to parse OSM data (see {@link OsmServerReader#parseOsm})
114 * @param newLayer true, if the data is to be downloaded into a new layer. If false, the task
115 * selects one of the existing layers as download layer, preferably the active layer.
116 * @param downloadArea the area to download
117 * @param progressMonitor the progressMonitor
118 * @return the future representing the asynchronous task
119 */
120 public Future<?> download(OsmServerReader reader, boolean newLayer, Bounds downloadArea, ProgressMonitor progressMonitor) {
121 return download(new DownloadTask(newLayer, reader, progressMonitor, zoomAfterDownload), downloadArea);
122 }
123
124 protected Future<?> download(DownloadTask downloadTask, Bounds downloadArea) {
125 this.downloadTask = downloadTask;
126 this.currentBounds = new Bounds(downloadArea);
127 // We need submit instead of execute so we can wait for it to finish and get the error
128 // message if necessary. If no one calls getErrorMessage() it just behaves like execute.
129 return MainApplication.worker.submit(downloadTask);
130 }
131
132 /**
133 * This allows subclasses to perform operations on the URL before {@link #loadUrl} is performed.
134 * @param url the original URL
135 * @return the modified URL
136 */
137 protected String modifyUrlBeforeLoad(String url) {
138 return url;
139 }
140
141 /**
142 * Loads a given URL from the OSM Server
143 * @param newLayer True if the data should be saved to a new layer
144 * @param url The URL as String
145 */
146 @Override
147 public Future<?> loadUrl(boolean newLayer, String url, ProgressMonitor progressMonitor) {
148 String newUrl = modifyUrlBeforeLoad(url);
149 downloadTask = new DownloadTask(newLayer,
150 new OsmServerLocationReader(newUrl),
151 progressMonitor);
152 currentBounds = null;
153 // Extract .osm filename from URL to set the new layer name
154 extractOsmFilename("https?://.*/(.*\\.osm)", newUrl);
155 return MainApplication.worker.submit(downloadTask);
156 }
157
158 protected final void extractOsmFilename(String pattern, String url) {
159 Matcher matcher = Pattern.compile(pattern).matcher(url);
160 newLayerName = matcher.matches() ? matcher.group(1) : null;
161 }
162
163 @Override
164 public void cancel() {
165 if (downloadTask != null) {
166 downloadTask.cancel();
167 }
168 }
169
170 @Override
171 public boolean isSafeForRemotecontrolRequests() {
172 return true;
173 }
174
175 @Override
176 public ProjectionBounds getDownloadProjectionBounds() {
177 return downloadTask != null ? downloadTask.computeBbox(currentBounds) : null;
178 }
179
180 /**
181 * Superclass of internal download task.
182 * @since 7636
183 */
184 public abstract static class AbstractInternalTask extends PleaseWaitRunnable {
185
186 protected final boolean newLayer;
187 protected final boolean zoomAfterDownload;
188 protected DataSet dataSet;
189
190 /**
191 * Constructs a new {@code AbstractInternalTask}.
192 *
193 * @param newLayer if {@code true}, force download to a new layer
194 * @param title message for the user
195 * @param ignoreException If true, exception will be propagated to calling code. If false then
196 * exception will be thrown directly in EDT. When this runnable is executed using executor framework
197 * then use false unless you read result of task (because exception will get lost if you don't)
198 * @param zoomAfterDownload If true, the map view will zoom to download area after download
199 */
200 public AbstractInternalTask(boolean newLayer, String title, boolean ignoreException, boolean zoomAfterDownload) {
201 super(title, ignoreException);
202 this.newLayer = newLayer;
203 this.zoomAfterDownload = zoomAfterDownload;
204 }
205
206 /**
207 * Constructs a new {@code AbstractInternalTask}.
208 *
209 * @param newLayer if {@code true}, force download to a new layer
210 * @param title message for the user
211 * @param progressMonitor progress monitor
212 * @param ignoreException If true, exception will be propagated to calling code. If false then
213 * exception will be thrown directly in EDT. When this runnable is executed using executor framework
214 * then use false unless you read result of task (because exception will get lost if you don't)
215 * @param zoomAfterDownload If true, the map view will zoom to download area after download
216 */
217 public AbstractInternalTask(boolean newLayer, String title, ProgressMonitor progressMonitor, boolean ignoreException,
218 boolean zoomAfterDownload) {
219 super(title, progressMonitor, ignoreException);
220 this.newLayer = newLayer;
221 this.zoomAfterDownload = zoomAfterDownload;
222 }
223
224 protected OsmDataLayer getEditLayer() {
225 if (!MainApplication.isDisplayingMapView()) return null;
226 return MainApplication.getLayerManager().getEditLayer();
227 }
228
229 protected int getNumDataLayers() {
230 return MainApplication.getLayerManager().getLayersOfType(OsmDataLayer.class).size();
231 }
232
233 protected OsmDataLayer getFirstDataLayer() {
234 return Utils.find(MainApplication.getLayerManager().getLayers(), OsmDataLayer.class);
235 }
236
237 protected OsmDataLayer createNewLayer(String layerName) {
238 if (layerName == null || layerName.isEmpty()) {
239 layerName = OsmDataLayer.createNewName();
240 }
241 return new OsmDataLayer(dataSet, layerName, null);
242 }
243
244 protected OsmDataLayer createNewLayer() {
245 return createNewLayer(null);
246 }
247
248 protected ProjectionBounds computeBbox(Bounds bounds) {
249 BoundingXYVisitor v = new BoundingXYVisitor();
250 if (bounds != null) {
251 v.visit(bounds);
252 } else {
253 v.computeBoundingBox(dataSet.getNodes());
254 }
255 return v.getBounds();
256 }
257
258 protected OsmDataLayer addNewLayerIfRequired(String newLayerName) {
259 int numDataLayers = getNumDataLayers();
260 if (newLayer || numDataLayers == 0 || (numDataLayers > 1 && getEditLayer() == null)) {
261 // the user explicitly wants a new layer, we don't have any layer at all
262 // or it is not clear which layer to merge to
263 //
264 final OsmDataLayer layer = createNewLayer(newLayerName);
265 MainApplication.getLayerManager().addLayer(layer, zoomAfterDownload);
266 return layer;
267 }
268 return null;
269 }
270
271 protected void loadData(String newLayerName, Bounds bounds) {
272 OsmDataLayer layer = addNewLayerIfRequired(newLayerName);
273 if (layer == null) {
274 layer = Optional.ofNullable(getEditLayer()).orElseGet(this::getFirstDataLayer);
275 Collection<OsmPrimitive> primitivesToUpdate = searchPrimitivesToUpdate(bounds, layer.data);
276 layer.mergeFrom(dataSet);
277 MapFrame map = MainApplication.getMap();
278 if (map != null && zoomAfterDownload && bounds != null) {
279 map.mapView.zoomTo(new ViewportData(computeBbox(bounds)));
280 }
281 if (!primitivesToUpdate.isEmpty()) {
282 MainApplication.worker.submit(new UpdatePrimitivesTask(layer, primitivesToUpdate));
283 }
284 layer.onPostDownloadFromServer();
285 }
286 }
287
288 /**
289 * Look for primitives deleted on server (thus absent from downloaded data)
290 * but still present in existing data layer
291 * @param bounds download bounds
292 * @param ds existing data set
293 * @return the primitives to update
294 */
295 private Collection<OsmPrimitive> searchPrimitivesToUpdate(Bounds bounds, DataSet ds) {
296 if (bounds == null)
297 return Collections.emptySet();
298 Collection<OsmPrimitive> col = new ArrayList<>();
299 ds.searchNodes(bounds.toBBox()).stream().filter(n -> !n.isNew() && !dataSet.containsNode(n)).forEachOrdered(col::add);
300 if (!col.isEmpty()) {
301 Set<Way> ways = new HashSet<>();
302 Set<Relation> rels = new HashSet<>();
303 for (OsmPrimitive n : col) {
304 for (OsmPrimitive ref : n.getReferrers()) {
305 if (ref.isNew()) {
306 continue;
307 } else if (ref instanceof Way) {
308 ways.add((Way) ref);
309 } else if (ref instanceof Relation) {
310 rels.add((Relation) ref);
311 }
312 }
313 }
314 ways.stream().filter(w -> !dataSet.containsWay(w)).forEachOrdered(col::add);
315 rels.stream().filter(r -> !dataSet.containsRelation(r)).forEachOrdered(col::add);
316 }
317 return col;
318 }
319 }
320
321 protected class DownloadTask extends AbstractInternalTask {
322 protected final OsmServerReader reader;
323
324 /**
325 * Constructs a new {@code DownloadTask}.
326 * @param newLayer if {@code true}, force download to a new layer
327 * @param reader OSM data reader
328 * @param progressMonitor progress monitor
329 */
330 public DownloadTask(boolean newLayer, OsmServerReader reader, ProgressMonitor progressMonitor) {
331 this(newLayer, reader, progressMonitor, true);
332 }
333
334 /**
335 * Constructs a new {@code DownloadTask}.
336 * @param newLayer if {@code true}, force download to a new layer
337 * @param reader OSM data reader
338 * @param progressMonitor progress monitor
339 * @param zoomAfterDownload If true, the map view will zoom to download area after download
340 * @since 8942
341 */
342 public DownloadTask(boolean newLayer, OsmServerReader reader, ProgressMonitor progressMonitor, boolean zoomAfterDownload) {
343 super(newLayer, tr("Downloading data"), progressMonitor, false, zoomAfterDownload);
344 this.reader = reader;
345 }
346
347 protected DataSet parseDataSet() throws OsmTransferException {
348 return reader.parseOsm(progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
349 }
350
351 @Override
352 public void realRun() throws IOException, SAXException, OsmTransferException {
353 try {
354 if (isCanceled())
355 return;
356 dataSet = parseDataSet();
357 } catch (OsmTransferException e) {
358 if (isCanceled()) {
359 Logging.info(tr("Ignoring exception because download has been canceled. Exception was: {0}", e.toString()));
360 return;
361 }
362 if (e instanceof OsmTransferCanceledException) {
363 setCanceled(true);
364 return;
365 } else {
366 rememberException(e);
367 }
368 DownloadOsmTask.this.setFailed(true);
369 }
370 }
371
372 @Override
373 protected void finish() {
374 if (isFailed() || isCanceled())
375 return;
376 if (dataSet == null)
377 return; // user canceled download or error occurred
378 if (dataSet.allPrimitives().isEmpty()) {
379 if (warnAboutEmptyArea) {
380 rememberErrorMessage(tr("No data found in this area."));
381 }
382 // need to synthesize a download bounds lest the visual indication of downloaded area doesn't work
383 dataSet.addDataSource(new DataSource(currentBounds != null ? currentBounds :
384 new Bounds(LatLon.ZERO), "OpenStreetMap server"));
385 }
386
387 rememberDownloadedData(dataSet);
388 loadData(newLayerName, currentBounds);
389 }
390
391 @Override
392 protected void cancel() {
393 setCanceled(true);
394 if (reader != null) {
395 reader.cancel();
396 }
397 }
398 }
399
400 @Override
401 public String getConfirmationMessage(URL url) {
402 if (url != null) {
403 String urlString = url.toExternalForm();
404 if (urlString.matches(OsmUrlPattern.OSM_API_URL.pattern())) {
405 // TODO: proper i18n after stabilization
406 Collection<String> items = new ArrayList<>();
407 items.add(tr("OSM Server URL:") + ' ' + url.getHost());
408 items.add(tr("Command")+": "+url.getPath());
409 if (url.getQuery() != null) {
410 items.add(tr("Request details: {0}", url.getQuery().replaceAll(",\\s*", ", ")));
411 }
412 return Utils.joinAsHtmlUnorderedList(items);
413 }
414 // TODO: other APIs
415 }
416 return null;
417 }
418}
Note: See TracBrowser for help on using the repository browser.