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.LinkedHashSet; 014import java.util.LinkedList; 015import java.util.List; 016import java.util.Objects; 017import java.util.Set; 018import java.util.concurrent.CancellationException; 019import java.util.concurrent.ExecutionException; 020import java.util.concurrent.Future; 021import java.util.stream.Collectors; 022 023import javax.swing.JOptionPane; 024 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 final boolean zoomAfterDownload; 053 private ProgressMonitor progressMonitor; 054 055 /** 056 * Constructs a new {@code DownloadTaskList}. Zooms to each download area. 057 */ 058 public DownloadTaskList() { 059 this(true); 060 } 061 062 /** 063 * Constructs a new {@code DownloadTaskList}. 064 * @param zoomAfterDownload whether to zoom to each download area 065 * @since 15205 066 */ 067 public DownloadTaskList(boolean zoomAfterDownload) { 068 this.zoomAfterDownload = zoomAfterDownload; 069 } 070 071 private void addDownloadTask(ProgressMonitor progressMonitor, DownloadTask dt, Rectangle2D td, int i, int n) { 072 ProgressMonitor childProgress = progressMonitor.createSubTaskMonitor(1, false); 073 childProgress.setCustomText(tr("Download {0} of {1} ({2} left)", i, n, n - i)); 074 dt.setZoomAfterDownload(zoomAfterDownload); 075 Future<?> future = dt.download(new DownloadParams(), new Bounds(td), childProgress); 076 taskFutures.add(future); 077 tasks.add(dt); 078 } 079 080 /** 081 * Downloads a list of areas from the OSM Server 082 * @param newLayer Set to true if all areas should be put into a single new layer 083 * @param rects The List of Rectangle2D to download 084 * @param osmData Set to true if OSM data should be downloaded 085 * @param gpxData Set to true if GPX data should be downloaded 086 * @param progressMonitor The progress monitor 087 * @return The Future representing the asynchronous download task 088 */ 089 public Future<?> download(boolean newLayer, List<Rectangle2D> rects, boolean osmData, boolean gpxData, ProgressMonitor progressMonitor) { 090 this.progressMonitor = progressMonitor; 091 if (newLayer) { 092 Layer l = new OsmDataLayer(new DataSet(), OsmDataLayer.createNewName(), null); 093 MainApplication.getLayerManager().addLayer(l); 094 MainApplication.getLayerManager().setActiveLayer(l); 095 } 096 097 int n = (osmData && gpxData ? 2 : 1)*rects.size(); 098 progressMonitor.beginTask(null, n); 099 int i = 0; 100 for (Rectangle2D td : rects) { 101 i++; 102 if (osmData) { 103 addDownloadTask(progressMonitor, new DownloadOsmTask(), td, i, n); 104 } 105 if (gpxData) { 106 addDownloadTask(progressMonitor, new DownloadGpsTask(), td, i, n); 107 } 108 } 109 progressMonitor.addCancelListener(() -> { 110 for (DownloadTask dt : tasks) { 111 dt.cancel(); 112 } 113 }); 114 return MainApplication.worker.submit(new PostDownloadProcessor(osmData)); 115 } 116 117 /** 118 * Downloads a list of areas from the OSM Server 119 * @param newLayer Set to true if all areas should be put into a single new layer 120 * @param areas The Collection of Areas to download 121 * @param osmData Set to true if OSM data should be downloaded 122 * @param gpxData Set to true if GPX data should be downloaded 123 * @param progressMonitor The progress monitor 124 * @return The Future representing the asynchronous download task 125 */ 126 public Future<?> download(boolean newLayer, Collection<Area> areas, boolean osmData, boolean gpxData, ProgressMonitor progressMonitor) { 127 progressMonitor.beginTask(tr("Updating data")); 128 try { 129 List<Rectangle2D> rects = areas.stream().map(Area::getBounds2D).collect(Collectors.toList()); 130 return download(newLayer, rects, osmData, gpxData, progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)); 131 } finally { 132 progressMonitor.finishTask(); 133 } 134 } 135 136 /** 137 * Replies the set of ids of all complete, non-new primitives (i.e. those with !primitive.incomplete) 138 * @param ds data set 139 * 140 * @return the set of ids of all complete, non-new primitives 141 */ 142 protected Set<OsmPrimitive> getCompletePrimitives(DataSet ds) { 143 return ds.allPrimitives().stream().filter(p -> !p.isIncomplete() && !p.isNew()).collect(Collectors.toSet()); 144 } 145 146 /** 147 * Updates the local state of a set of primitives (given by a set of primitive ids) with the 148 * state currently held on the server. 149 * 150 * @param potentiallyDeleted a set of ids to check update from the server 151 */ 152 protected void updatePotentiallyDeletedPrimitives(Set<OsmPrimitive> potentiallyDeleted) { 153 final List<OsmPrimitive> toSelect = new ArrayList<>(); 154 for (OsmPrimitive primitive : potentiallyDeleted) { 155 if (primitive != null) { 156 toSelect.add(primitive); 157 } 158 } 159 EventQueue.invokeLater(() -> UpdateSelectionAction.updatePrimitives(toSelect)); 160 } 161 162 /** 163 * Processes a set of primitives (given by a set of their ids) which might be deleted on the 164 * server. First prompts the user whether he wants to check the current state on the server. If 165 * yes, retrieves the current state on the server and checks whether the primitives are indeed 166 * deleted on the server. 167 * 168 * @param potentiallyDeleted a set of primitives (given by their ids) 169 */ 170 protected void handlePotentiallyDeletedPrimitives(Set<OsmPrimitive> potentiallyDeleted) { 171 ButtonSpec[] options = new ButtonSpec[] { 172 new ButtonSpec( 173 tr("Check on the server"), 174 new ImageProvider("ok"), 175 tr("Click to check whether objects in your local dataset are deleted on the server"), 176 null /* no specific help topic */), 177 new ButtonSpec( 178 tr("Ignore"), 179 new ImageProvider("cancel"), 180 tr("Click to abort and to resume editing"), 181 null /* no specific help topic */), 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 MainApplication.getMainFrame(), 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 return tasks.stream() 222 .filter(t -> t instanceof DownloadOsmTask) 223 .map(t -> ((DownloadOsmTask) t).getDownloadedData()) 224 .filter(Objects::nonNull) 225 .flatMap(ds -> ds.allPrimitives().stream()) 226 .collect(Collectors.toSet()); 227 } 228 229 class PostDownloadProcessor implements Runnable { 230 231 private final boolean osmData; 232 233 PostDownloadProcessor(boolean osmData) { 234 this.osmData = osmData; 235 } 236 237 /** 238 * Grabs and displays the error messages after all download threads have finished. 239 */ 240 @Override 241 public void run() { 242 progressMonitor.finishTask(); 243 244 // wait for all download tasks to finish 245 // 246 for (Future<?> future : taskFutures) { 247 try { 248 future.get(); 249 } catch (InterruptedException | ExecutionException | CancellationException e) { 250 Logging.error(e); 251 return; 252 } 253 } 254 Set<Object> errors = new LinkedHashSet<>(); 255 for (DownloadTask dt : tasks) { 256 errors.addAll(dt.getErrorObjects()); 257 } 258 if (!errors.isEmpty()) { 259 final Collection<String> items = new ArrayList<>(); 260 for (Object error : errors) { 261 if (error instanceof String) { 262 items.add((String) error); 263 } else if (error instanceof Exception) { 264 items.add(ExceptionUtil.explainException((Exception) error)); 265 } 266 } 267 268 GuiHelper.runInEDT(() -> { 269 if (items.size() == 1 && tr("No data found in this area.").equals(items.iterator().next())) { 270 new Notification(items.iterator().next()).setIcon(JOptionPane.WARNING_MESSAGE).show(); 271 } else { 272 JOptionPane.showMessageDialog(MainApplication.getMainFrame(), "<html>" 273 + tr("The following errors occurred during mass download: {0}", 274 Utils.joinAsHtmlUnorderedList(items)) + "</html>", 275 tr("Errors during download"), JOptionPane.ERROR_MESSAGE); 276 } 277 }); 278 279 return; 280 } 281 282 // FIXME: this is a hack. We assume that the user canceled the whole download if at 283 // least one task was canceled or if it failed 284 // 285 for (DownloadTask task : tasks) { 286 if (task instanceof AbstractDownloadTask) { 287 AbstractDownloadTask<?> absTask = (AbstractDownloadTask<?>) task; 288 if (absTask.isCanceled() || absTask.isFailed()) 289 return; 290 } 291 } 292 final OsmDataLayer editLayer = MainApplication.getLayerManager().getEditLayer(); 293 if (editLayer != null && osmData) { 294 final Set<OsmPrimitive> myPrimitives = getCompletePrimitives(editLayer.getDataSet()); 295 for (DownloadTask task : tasks) { 296 if (task instanceof DownloadOsmTask) { 297 DataSet ds = ((DownloadOsmTask) task).getDownloadedData(); 298 if (ds != null) { 299 // myPrimitives.removeAll(ds.allPrimitives()) will do the same job but much slower 300 for (OsmPrimitive primitive: ds.allPrimitives()) { 301 myPrimitives.remove(primitive); 302 } 303 } 304 } 305 } 306 if (!myPrimitives.isEmpty()) { 307 GuiHelper.runInEDT(() -> handlePotentiallyDeletedPrimitives(myPrimitives)); 308 } 309 } 310 } 311 } 312}