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