001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions.downloadtasks; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.tr; 006import static org.openstreetmap.josm.tools.I18n.trn; 007 008import java.awt.EventQueue; 009import java.awt.geom.Area; 010import java.awt.geom.Rectangle2D; 011import java.util.ArrayList; 012import java.util.Collection; 013import java.util.HashSet; 014import java.util.LinkedHashSet; 015import java.util.LinkedList; 016import java.util.List; 017import java.util.Set; 018import java.util.concurrent.CancellationException; 019import java.util.concurrent.ExecutionException; 020import java.util.concurrent.Future; 021 022import javax.swing.JOptionPane; 023 024import org.openstreetmap.josm.Main; 025import org.openstreetmap.josm.actions.UpdateSelectionAction; 026import org.openstreetmap.josm.data.Bounds; 027import org.openstreetmap.josm.data.osm.DataSet; 028import org.openstreetmap.josm.data.osm.OsmPrimitive; 029import org.openstreetmap.josm.gui.HelpAwareOptionPane; 030import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec; 031import org.openstreetmap.josm.gui.Notification; 032import org.openstreetmap.josm.gui.layer.Layer; 033import org.openstreetmap.josm.gui.layer.OsmDataLayer; 034import org.openstreetmap.josm.gui.progress.ProgressMonitor; 035import org.openstreetmap.josm.gui.util.GuiHelper; 036import org.openstreetmap.josm.tools.ExceptionUtil; 037import org.openstreetmap.josm.tools.ImageProvider; 038import org.openstreetmap.josm.tools.Utils; 039 040/** 041 * This class encapsulates the downloading of several bounding boxes that would otherwise be too 042 * large to download in one go. Error messages will be collected for all downloads and displayed as 043 * a list in the end. 044 * @author xeen 045 * @since 6053 046 */ 047public class DownloadTaskList { 048 private final List<DownloadTask> tasks = new LinkedList<>(); 049 private final List<Future<?>> taskFutures = new LinkedList<>(); 050 private ProgressMonitor progressMonitor; 051 052 private void addDownloadTask(ProgressMonitor progressMonitor, DownloadTask dt, Rectangle2D td, int i, int n) { 053 ProgressMonitor childProgress = progressMonitor.createSubTaskMonitor(1, false); 054 childProgress.setCustomText(tr("Download {0} of {1} ({2} left)", i, n, n - i)); 055 Future<?> future = dt.download(false, new Bounds(td), childProgress); 056 taskFutures.add(future); 057 tasks.add(dt); 058 } 059 060 /** 061 * Downloads a list of areas from the OSM Server 062 * @param newLayer Set to true if all areas should be put into a single new layer 063 * @param rects The List of Rectangle2D to download 064 * @param osmData Set to true if OSM data should be downloaded 065 * @param gpxData Set to true if GPX data should be downloaded 066 * @param progressMonitor The progress monitor 067 * @return The Future representing the asynchronous download task 068 */ 069 public Future<?> download(boolean newLayer, List<Rectangle2D> rects, boolean osmData, boolean gpxData, ProgressMonitor progressMonitor) { 070 this.progressMonitor = progressMonitor; 071 if (newLayer) { 072 Layer l = new OsmDataLayer(new DataSet(), OsmDataLayer.createNewName(), null); 073 Main.getLayerManager().addLayer(l); 074 Main.getLayerManager().setActiveLayer(l); 075 } 076 077 int n = (osmData && gpxData ? 2 : 1)*rects.size(); 078 progressMonitor.beginTask(null, n); 079 int i = 0; 080 for (Rectangle2D td : rects) { 081 i++; 082 if (osmData) { 083 addDownloadTask(progressMonitor, new DownloadOsmTask(), td, i, n); 084 } 085 if (gpxData) { 086 addDownloadTask(progressMonitor, new DownloadGpsTask(), td, i, n); 087 } 088 } 089 progressMonitor.addCancelListener(() -> { 090 for (DownloadTask dt : tasks) { 091 dt.cancel(); 092 } 093 }); 094 return Main.worker.submit(new PostDownloadProcessor(osmData)); 095 } 096 097 /** 098 * Downloads a list of areas from the OSM Server 099 * @param newLayer Set to true if all areas should be put into a single new layer 100 * @param areas The Collection of Areas to download 101 * @param osmData Set to true if OSM data should be downloaded 102 * @param gpxData Set to true if GPX data should be downloaded 103 * @param progressMonitor The progress monitor 104 * @return The Future representing the asynchronous download task 105 */ 106 public Future<?> download(boolean newLayer, Collection<Area> areas, boolean osmData, boolean gpxData, ProgressMonitor progressMonitor) { 107 progressMonitor.beginTask(tr("Updating data")); 108 try { 109 List<Rectangle2D> rects = new ArrayList<>(areas.size()); 110 for (Area a : areas) { 111 rects.add(a.getBounds2D()); 112 } 113 114 return download(newLayer, rects, osmData, gpxData, progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)); 115 } finally { 116 progressMonitor.finishTask(); 117 } 118 } 119 120 /** 121 * Replies the set of ids of all complete, non-new primitives (i.e. those with !primitive.incomplete) 122 * @param ds data set 123 * 124 * @return the set of ids of all complete, non-new primitives 125 */ 126 protected Set<OsmPrimitive> getCompletePrimitives(DataSet ds) { 127 Set<OsmPrimitive> ret = new HashSet<>(); 128 for (OsmPrimitive primitive : ds.allPrimitives()) { 129 if (!primitive.isIncomplete() && !primitive.isNew()) { 130 ret.add(primitive); 131 } 132 } 133 return ret; 134 } 135 136 /** 137 * Updates the local state of a set of primitives (given by a set of primitive ids) with the 138 * state currently held on the server. 139 * 140 * @param potentiallyDeleted a set of ids to check update from the server 141 */ 142 protected void updatePotentiallyDeletedPrimitives(Set<OsmPrimitive> potentiallyDeleted) { 143 final List<OsmPrimitive> toSelect = new ArrayList<>(); 144 for (OsmPrimitive primitive : potentiallyDeleted) { 145 if (primitive != null) { 146 toSelect.add(primitive); 147 } 148 } 149 EventQueue.invokeLater(() -> UpdateSelectionAction.updatePrimitives(toSelect)); 150 } 151 152 /** 153 * Processes a set of primitives (given by a set of their ids) which might be deleted on the 154 * server. First prompts the user whether he wants to check the current state on the server. If 155 * yes, retrieves the current state on the server and checks whether the primitives are indeed 156 * deleted on the server. 157 * 158 * @param potentiallyDeleted a set of primitives (given by their ids) 159 */ 160 protected void handlePotentiallyDeletedPrimitives(Set<OsmPrimitive> potentiallyDeleted) { 161 ButtonSpec[] options = new ButtonSpec[] { 162 new ButtonSpec( 163 tr("Check on the server"), 164 ImageProvider.get("ok"), 165 tr("Click to check whether objects in your local dataset are deleted on the server"), 166 null /* no specific help topic */ 167 ), 168 new ButtonSpec( 169 tr("Ignore"), 170 ImageProvider.get("cancel"), 171 tr("Click to abort and to resume editing"), 172 null /* no specific help topic */ 173 ), 174 }; 175 176 String message = "<html>" + trn( 177 "There is {0} object in your local dataset which " 178 + "might be deleted on the server.<br>If you later try to delete or " 179 + "update this the server is likely to report a conflict.", 180 "There are {0} objects in your local dataset which " 181 + "might be deleted on the server.<br>If you later try to delete or " 182 + "update them the server is likely to report a conflict.", 183 potentiallyDeleted.size(), potentiallyDeleted.size()) 184 + "<br>" 185 + trn("Click <strong>{0}</strong> to check the state of this object on the server.", 186 "Click <strong>{0}</strong> to check the state of these objects on the server.", 187 potentiallyDeleted.size(), 188 options[0].text) + "<br>" 189 + tr("Click <strong>{0}</strong> to ignore." + "</html>", options[1].text); 190 191 int ret = HelpAwareOptionPane.showOptionDialog( 192 Main.parent, 193 message, 194 tr("Deleted or moved objects"), 195 JOptionPane.WARNING_MESSAGE, 196 null, 197 options, 198 options[0], 199 ht("/Action/UpdateData#SyncPotentiallyDeletedObjects") 200 ); 201 if (ret != 0 /* OK */) 202 return; 203 204 updatePotentiallyDeletedPrimitives(potentiallyDeleted); 205 } 206 207 /** 208 * Replies the set of primitive ids which have been downloaded by this task list 209 * 210 * @return the set of primitive ids which have been downloaded by this task list 211 */ 212 public Set<OsmPrimitive> getDownloadedPrimitives() { 213 Set<OsmPrimitive> ret = new HashSet<>(); 214 for (DownloadTask task : tasks) { 215 if (task instanceof DownloadOsmTask) { 216 DataSet ds = ((DownloadOsmTask) task).getDownloadedData(); 217 if (ds != null) { 218 ret.addAll(ds.allPrimitives()); 219 } 220 } 221 } 222 return ret; 223 } 224 225 class PostDownloadProcessor implements Runnable { 226 227 private final boolean osmData; 228 229 PostDownloadProcessor(boolean osmData) { 230 this.osmData = osmData; 231 } 232 233 /** 234 * Grabs and displays the error messages after all download threads have finished. 235 */ 236 @Override 237 public void run() { 238 progressMonitor.finishTask(); 239 240 // wait for all download tasks to finish 241 // 242 for (Future<?> future : taskFutures) { 243 try { 244 future.get(); 245 } catch (InterruptedException | ExecutionException | CancellationException e) { 246 Main.error(e); 247 return; 248 } 249 } 250 Set<Object> errors = new LinkedHashSet<>(); 251 for (DownloadTask dt : tasks) { 252 errors.addAll(dt.getErrorObjects()); 253 } 254 if (!errors.isEmpty()) { 255 final Collection<String> items = new ArrayList<>(); 256 for (Object error : errors) { 257 if (error instanceof String) { 258 items.add((String) error); 259 } else if (error instanceof Exception) { 260 items.add(ExceptionUtil.explainException((Exception) error)); 261 } 262 } 263 264 GuiHelper.runInEDT(() -> { 265 if (items.size() == 1 && tr("No data found in this area.").equals(items.iterator().next())) { 266 new Notification(items.iterator().next()).setIcon(JOptionPane.WARNING_MESSAGE).show(); 267 } else { 268 JOptionPane.showMessageDialog(Main.parent, "<html>" 269 + tr("The following errors occurred during mass download: {0}", 270 Utils.joinAsHtmlUnorderedList(items)) + "</html>", 271 tr("Errors during download"), JOptionPane.ERROR_MESSAGE); 272 } 273 }); 274 275 return; 276 } 277 278 // FIXME: this is a hack. We assume that the user canceled the whole download if at 279 // least one task was canceled or if it failed 280 // 281 for (DownloadTask task : tasks) { 282 if (task instanceof AbstractDownloadTask) { 283 AbstractDownloadTask<?> absTask = (AbstractDownloadTask<?>) task; 284 if (absTask.isCanceled() || absTask.isFailed()) 285 return; 286 } 287 } 288 final OsmDataLayer editLayer = Main.getLayerManager().getEditLayer(); 289 if (editLayer != null && osmData) { 290 final Set<OsmPrimitive> myPrimitives = getCompletePrimitives(editLayer.data); 291 for (DownloadTask task : tasks) { 292 if (task instanceof DownloadOsmTask) { 293 DataSet ds = ((DownloadOsmTask) task).getDownloadedData(); 294 if (ds != null) { 295 // myPrimitives.removeAll(ds.allPrimitives()) will do the same job but much slower 296 for (OsmPrimitive primitive: ds.allPrimitives()) { 297 myPrimitives.remove(primitive); 298 } 299 } 300 } 301 } 302 if (!myPrimitives.isEmpty()) { 303 GuiHelper.runInEDT(() -> handlePotentiallyDeletedPrimitives(myPrimitives)); 304 } 305 } 306 } 307 } 308}