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    }