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