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