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

Last change on this file since 13928 was 13928, checked in by Don-vip, 6 years ago

see #11000 - support layer_name in the general case

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