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