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