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

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

fix #5092 - Synchronization issue with nodes deleted on server

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