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