// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.actions.downloadtasks; import static org.openstreetmap.josm.gui.help.HelpUtil.ht; import static org.openstreetmap.josm.tools.I18n.tr; import static org.openstreetmap.josm.tools.I18n.trn; import java.awt.EventQueue; import java.awt.geom.Area; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import javax.swing.JOptionPane; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.actions.UpdateSelectionAction; import org.openstreetmap.josm.data.Bounds; import org.openstreetmap.josm.data.osm.DataSet; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.gui.HelpAwareOptionPane; import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec; import org.openstreetmap.josm.gui.Notification; import org.openstreetmap.josm.gui.layer.Layer; import org.openstreetmap.josm.gui.layer.OsmDataLayer; import org.openstreetmap.josm.gui.progress.ProgressMonitor; import org.openstreetmap.josm.gui.util.GuiHelper; import org.openstreetmap.josm.tools.ExceptionUtil; import org.openstreetmap.josm.tools.ImageProvider; import org.openstreetmap.josm.tools.Utils; /** * This class encapsulates the downloading of several bounding boxes that would otherwise be too * large to download in one go. Error messages will be collected for all downloads and displayed as * a list in the end. * @author xeen * @since 6053 */ public class DownloadTaskList { private final List<DownloadTask> tasks = new LinkedList<>(); private final List<Future<?>> taskFutures = new LinkedList<>(); private ProgressMonitor progressMonitor; private void addDownloadTask(ProgressMonitor progressMonitor, DownloadTask dt, Rectangle2D td, int i, int n) { ProgressMonitor childProgress = progressMonitor.createSubTaskMonitor(1, false); childProgress.setCustomText(tr("Download {0} of {1} ({2} left)", i, n, n - i)); Future<?> future = dt.download(false, new Bounds(td), childProgress); taskFutures.add(future); tasks.add(dt); } /** * Downloads a list of areas from the OSM Server * @param newLayer Set to true if all areas should be put into a single new layer * @param rects The List of Rectangle2D to download * @param osmData Set to true if OSM data should be downloaded * @param gpxData Set to true if GPX data should be downloaded * @param progressMonitor The progress monitor * @return The Future representing the asynchronous download task */ public Future<?> download(boolean newLayer, List<Rectangle2D> rects, boolean osmData, boolean gpxData, ProgressMonitor progressMonitor) { this.progressMonitor = progressMonitor; if (newLayer) { Layer l = new OsmDataLayer(new DataSet(), OsmDataLayer.createNewName(), null); Main.getLayerManager().addLayer(l); Main.getLayerManager().setActiveLayer(l); } int n = (osmData && gpxData ? 2 : 1)*rects.size(); progressMonitor.beginTask(null, n); int i = 0; for (Rectangle2D td : rects) { i++; if (osmData) { addDownloadTask(progressMonitor, new DownloadOsmTask(), td, i, n); } if (gpxData) { addDownloadTask(progressMonitor, new DownloadGpsTask(), td, i, n); } } progressMonitor.addCancelListener(() -> { for (DownloadTask dt : tasks) { dt.cancel(); } }); return Main.worker.submit(new PostDownloadProcessor(osmData)); } /** * Downloads a list of areas from the OSM Server * @param newLayer Set to true if all areas should be put into a single new layer * @param areas The Collection of Areas to download * @param osmData Set to true if OSM data should be downloaded * @param gpxData Set to true if GPX data should be downloaded * @param progressMonitor The progress monitor * @return The Future representing the asynchronous download task */ public Future<?> download(boolean newLayer, Collection<Area> areas, boolean osmData, boolean gpxData, ProgressMonitor progressMonitor) { progressMonitor.beginTask(tr("Updating data")); try { List<Rectangle2D> rects = new ArrayList<>(areas.size()); for (Area a : areas) { rects.add(a.getBounds2D()); } return download(newLayer, rects, osmData, gpxData, progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)); } finally { progressMonitor.finishTask(); } } /** * Replies the set of ids of all complete, non-new primitives (i.e. those with !primitive.incomplete) * @param ds data set * * @return the set of ids of all complete, non-new primitives */ protected Set<OsmPrimitive> getCompletePrimitives(DataSet ds) { Set<OsmPrimitive> ret = new HashSet<>(); for (OsmPrimitive primitive : ds.allPrimitives()) { if (!primitive.isIncomplete() && !primitive.isNew()) { ret.add(primitive); } } return ret; } /** * Updates the local state of a set of primitives (given by a set of primitive ids) with the * state currently held on the server. * * @param potentiallyDeleted a set of ids to check update from the server */ protected void updatePotentiallyDeletedPrimitives(Set<OsmPrimitive> potentiallyDeleted) { final List<OsmPrimitive> toSelect = new ArrayList<>(); for (OsmPrimitive primitive : potentiallyDeleted) { if (primitive != null) { toSelect.add(primitive); } } EventQueue.invokeLater(() -> UpdateSelectionAction.updatePrimitives(toSelect)); } /** * Processes a set of primitives (given by a set of their ids) which might be deleted on the * server. First prompts the user whether he wants to check the current state on the server. If * yes, retrieves the current state on the server and checks whether the primitives are indeed * deleted on the server. * * @param potentiallyDeleted a set of primitives (given by their ids) */ protected void handlePotentiallyDeletedPrimitives(Set<OsmPrimitive> potentiallyDeleted) { ButtonSpec[] options = new ButtonSpec[] { new ButtonSpec( tr("Check on the server"), ImageProvider.get("ok"), tr("Click to check whether objects in your local dataset are deleted on the server"), null /* no specific help topic */ ), new ButtonSpec( tr("Ignore"), ImageProvider.get("cancel"), tr("Click to abort and to resume editing"), null /* no specific help topic */ ), }; String message = "<html>" + trn( "There is {0} object in your local dataset which " + "might be deleted on the server.<br>If you later try to delete or " + "update this the server is likely to report a conflict.", "There are {0} objects in your local dataset which " + "might be deleted on the server.<br>If you later try to delete or " + "update them the server is likely to report a conflict.", potentiallyDeleted.size(), potentiallyDeleted.size()) + "<br>" + trn("Click <strong>{0}</strong> to check the state of this object on the server.", "Click <strong>{0}</strong> to check the state of these objects on the server.", potentiallyDeleted.size(), options[0].text) + "<br>" + tr("Click <strong>{0}</strong> to ignore." + "</html>", options[1].text); int ret = HelpAwareOptionPane.showOptionDialog( Main.parent, message, tr("Deleted or moved objects"), JOptionPane.WARNING_MESSAGE, null, options, options[0], ht("/Action/UpdateData#SyncPotentiallyDeletedObjects") ); if (ret != 0 /* OK */) return; updatePotentiallyDeletedPrimitives(potentiallyDeleted); } /** * Replies the set of primitive ids which have been downloaded by this task list * * @return the set of primitive ids which have been downloaded by this task list */ public Set<OsmPrimitive> getDownloadedPrimitives() { Set<OsmPrimitive> ret = new HashSet<>(); for (DownloadTask task : tasks) { if (task instanceof DownloadOsmTask) { DataSet ds = ((DownloadOsmTask) task).getDownloadedData(); if (ds != null) { ret.addAll(ds.allPrimitives()); } } } return ret; } class PostDownloadProcessor implements Runnable { private final boolean osmData; PostDownloadProcessor(boolean osmData) { this.osmData = osmData; } /** * Grabs and displays the error messages after all download threads have finished. */ @Override public void run() { progressMonitor.finishTask(); // wait for all download tasks to finish // for (Future<?> future : taskFutures) { try { future.get(); } catch (InterruptedException | ExecutionException | CancellationException e) { Main.error(e); return; } } Set<Object> errors = new LinkedHashSet<>(); for (DownloadTask dt : tasks) { errors.addAll(dt.getErrorObjects()); } if (!errors.isEmpty()) { final Collection<String> items = new ArrayList<>(); for (Object error : errors) { if (error instanceof String) { items.add((String) error); } else if (error instanceof Exception) { items.add(ExceptionUtil.explainException((Exception) error)); } } GuiHelper.runInEDT(() -> { if (items.size() == 1 && tr("No data found in this area.").equals(items.iterator().next())) { new Notification(items.iterator().next()).setIcon(JOptionPane.WARNING_MESSAGE).show(); } else { JOptionPane.showMessageDialog(Main.parent, "<html>" + tr("The following errors occurred during mass download: {0}", Utils.joinAsHtmlUnorderedList(items)) + "</html>", tr("Errors during download"), JOptionPane.ERROR_MESSAGE); } }); return; } // FIXME: this is a hack. We assume that the user canceled the whole download if at // least one task was canceled or if it failed // for (DownloadTask task : tasks) { if (task instanceof AbstractDownloadTask) { AbstractDownloadTask<?> absTask = (AbstractDownloadTask<?>) task; if (absTask.isCanceled() || absTask.isFailed()) return; } } final OsmDataLayer editLayer = Main.getLayerManager().getEditLayer(); if (editLayer != null && osmData) { final Set<OsmPrimitive> myPrimitives = getCompletePrimitives(editLayer.data); for (DownloadTask task : tasks) { if (task instanceof DownloadOsmTask) { DataSet ds = ((DownloadOsmTask) task).getDownloadedData(); if (ds != null) { // myPrimitives.removeAll(ds.allPrimitives()) will do the same job but much slower for (OsmPrimitive primitive: ds.allPrimitives()) { myPrimitives.remove(primitive); } } } } if (!myPrimitives.isEmpty()) { GuiHelper.runInEDT(() -> handlePotentiallyDeletedPrimitives(myPrimitives)); } } } } }