| 1 | // License: GPL. For details, see LICENSE file. |
|---|
| 2 | package org.openstreetmap.josm.actions.downloadtasks; |
|---|
| 3 | |
|---|
| 4 | import static org.openstreetmap.josm.gui.help.HelpUtil.ht; |
|---|
| 5 | import static org.openstreetmap.josm.tools.I18n.tr; |
|---|
| 6 | import static org.openstreetmap.josm.tools.I18n.trn; |
|---|
| 7 | |
|---|
| 8 | import java.awt.EventQueue; |
|---|
| 9 | import java.awt.geom.Area; |
|---|
| 10 | import java.awt.geom.Rectangle2D; |
|---|
| 11 | import java.util.ArrayList; |
|---|
| 12 | import java.util.Collection; |
|---|
| 13 | import java.util.HashSet; |
|---|
| 14 | import java.util.LinkedHashSet; |
|---|
| 15 | import java.util.LinkedList; |
|---|
| 16 | import java.util.List; |
|---|
| 17 | import java.util.Set; |
|---|
| 18 | import java.util.concurrent.Future; |
|---|
| 19 | |
|---|
| 20 | import javax.swing.JOptionPane; |
|---|
| 21 | import javax.swing.SwingUtilities; |
|---|
| 22 | |
|---|
| 23 | import org.openstreetmap.josm.Main; |
|---|
| 24 | import org.openstreetmap.josm.actions.UpdateSelectionAction; |
|---|
| 25 | import org.openstreetmap.josm.data.Bounds; |
|---|
| 26 | import org.openstreetmap.josm.data.osm.DataSet; |
|---|
| 27 | import org.openstreetmap.josm.data.osm.OsmPrimitive; |
|---|
| 28 | import org.openstreetmap.josm.gui.HelpAwareOptionPane; |
|---|
| 29 | import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec; |
|---|
| 30 | import org.openstreetmap.josm.gui.layer.Layer; |
|---|
| 31 | import org.openstreetmap.josm.gui.layer.OsmDataLayer; |
|---|
| 32 | import org.openstreetmap.josm.gui.progress.ProgressMonitor; |
|---|
| 33 | import org.openstreetmap.josm.gui.progress.ProgressMonitor.CancelListener; |
|---|
| 34 | import org.openstreetmap.josm.tools.ExceptionUtil; |
|---|
| 35 | import org.openstreetmap.josm.tools.ImageProvider; |
|---|
| 36 | |
|---|
| 37 | /** |
|---|
| 38 | * This class encapsulates the downloading of several bounding boxes that would otherwise be too |
|---|
| 39 | * large to download in one go. Error messages will be collected for all downloads and displayed as |
|---|
| 40 | * a list in the end. |
|---|
| 41 | * @author xeen |
|---|
| 42 | * |
|---|
| 43 | */ |
|---|
| 44 | public class DownloadOsmTaskList { |
|---|
| 45 | private List<DownloadTask> osmTasks = new LinkedList<DownloadTask>(); |
|---|
| 46 | private List<Future<?>> osmTaskFutures = new LinkedList<Future<?>>(); |
|---|
| 47 | private ProgressMonitor progressMonitor; |
|---|
| 48 | |
|---|
| 49 | /** |
|---|
| 50 | * Downloads a list of areas from the OSM Server |
|---|
| 51 | * @param newLayer Set to true if all areas should be put into a single new layer |
|---|
| 52 | * @param The List of Rectangle2D to download |
|---|
| 53 | */ |
|---|
| 54 | public Future<?> download(boolean newLayer, List<Rectangle2D> rects, ProgressMonitor progressMonitor) { |
|---|
| 55 | this.progressMonitor = progressMonitor; |
|---|
| 56 | if (newLayer) { |
|---|
| 57 | Layer l = new OsmDataLayer(new DataSet(), OsmDataLayer.createNewName(), null); |
|---|
| 58 | Main.main.addLayer(l); |
|---|
| 59 | Main.map.mapView.setActiveLayer(l); |
|---|
| 60 | } |
|---|
| 61 | |
|---|
| 62 | progressMonitor.beginTask(null, rects.size()); |
|---|
| 63 | int i = 0; |
|---|
| 64 | for (Rectangle2D td : rects) { |
|---|
| 65 | i++; |
|---|
| 66 | DownloadTask dt = new DownloadOsmTask(); |
|---|
| 67 | ProgressMonitor childProgress = progressMonitor.createSubTaskMonitor(1, false); |
|---|
| 68 | childProgress.setCustomText(tr("Download {0} of {1} ({2} left)", i, rects.size(), rects.size() - i)); |
|---|
| 69 | Future<?> future = dt.download(false, new Bounds(td), childProgress); |
|---|
| 70 | osmTaskFutures.add(future); |
|---|
| 71 | osmTasks.add(dt); |
|---|
| 72 | } |
|---|
| 73 | progressMonitor.addCancelListener(new CancelListener() { |
|---|
| 74 | public void operationCanceled() { |
|---|
| 75 | for (DownloadTask dt : osmTasks) { |
|---|
| 76 | dt.cancel(); |
|---|
| 77 | } |
|---|
| 78 | } |
|---|
| 79 | }); |
|---|
| 80 | return Main.worker.submit(new PostDownloadProcessor()); |
|---|
| 81 | } |
|---|
| 82 | |
|---|
| 83 | /** |
|---|
| 84 | * Downloads a list of areas from the OSM Server |
|---|
| 85 | * @param newLayer Set to true if all areas should be put into a single new layer |
|---|
| 86 | * @param The Collection of Areas to download |
|---|
| 87 | */ |
|---|
| 88 | public Future<?> download(boolean newLayer, Collection<Area> areas, ProgressMonitor progressMonitor) { |
|---|
| 89 | progressMonitor.beginTask(tr("Updating data")); |
|---|
| 90 | try { |
|---|
| 91 | List<Rectangle2D> rects = new ArrayList<Rectangle2D>(areas.size()); |
|---|
| 92 | for (Area a : areas) { |
|---|
| 93 | rects.add(a.getBounds2D()); |
|---|
| 94 | } |
|---|
| 95 | |
|---|
| 96 | return download(newLayer, rects, progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)); |
|---|
| 97 | } finally { |
|---|
| 98 | progressMonitor.finishTask(); |
|---|
| 99 | } |
|---|
| 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 | } |
|---|