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.CancellationException;
019import java.util.concurrent.ExecutionException;
020import java.util.concurrent.Future;
021
022import javax.swing.JOptionPane;
023
024import org.openstreetmap.josm.Main;
025import org.openstreetmap.josm.actions.UpdateSelectionAction;
026import org.openstreetmap.josm.data.Bounds;
027import org.openstreetmap.josm.data.osm.DataSet;
028import org.openstreetmap.josm.data.osm.OsmPrimitive;
029import org.openstreetmap.josm.gui.HelpAwareOptionPane;
030import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
031import org.openstreetmap.josm.gui.Notification;
032import org.openstreetmap.josm.gui.layer.Layer;
033import org.openstreetmap.josm.gui.layer.OsmDataLayer;
034import org.openstreetmap.josm.gui.progress.ProgressMonitor;
035import org.openstreetmap.josm.gui.util.GuiHelper;
036import org.openstreetmap.josm.tools.ExceptionUtil;
037import org.openstreetmap.josm.tools.ImageProvider;
038import org.openstreetmap.josm.tools.Utils;
039
040/**
041 * This class encapsulates the downloading of several bounding boxes that would otherwise be too
042 * large to download in one go. Error messages will be collected for all downloads and displayed as
043 * a list in the end.
044 * @author xeen
045 * @since 6053
046 */
047public class DownloadTaskList {
048    private final List<DownloadTask> tasks = new LinkedList<>();
049    private final List<Future<?>> taskFutures = new LinkedList<>();
050    private ProgressMonitor progressMonitor;
051
052    private void addDownloadTask(ProgressMonitor progressMonitor, DownloadTask dt, Rectangle2D td, int i, int n) {
053        ProgressMonitor childProgress = progressMonitor.createSubTaskMonitor(1, false);
054        childProgress.setCustomText(tr("Download {0} of {1} ({2} left)", i, n, n - i));
055        Future<?> future = dt.download(false, new Bounds(td), childProgress);
056        taskFutures.add(future);
057        tasks.add(dt);
058    }
059
060    /**
061     * Downloads a list of areas from the OSM Server
062     * @param newLayer Set to true if all areas should be put into a single new layer
063     * @param rects The List of Rectangle2D to download
064     * @param osmData Set to true if OSM data should be downloaded
065     * @param gpxData Set to true if GPX data should be downloaded
066     * @param progressMonitor The progress monitor
067     * @return The Future representing the asynchronous download task
068     */
069    public Future<?> download(boolean newLayer, List<Rectangle2D> rects, boolean osmData, boolean gpxData, ProgressMonitor progressMonitor) {
070        this.progressMonitor = progressMonitor;
071        if (newLayer) {
072            Layer l = new OsmDataLayer(new DataSet(), OsmDataLayer.createNewName(), null);
073            Main.getLayerManager().addLayer(l);
074            Main.getLayerManager().setActiveLayer(l);
075        }
076
077        int n = (osmData && gpxData ? 2 : 1)*rects.size();
078        progressMonitor.beginTask(null, n);
079        int i = 0;
080        for (Rectangle2D td : rects) {
081            i++;
082            if (osmData) {
083                addDownloadTask(progressMonitor, new DownloadOsmTask(), td, i, n);
084            }
085            if (gpxData) {
086                addDownloadTask(progressMonitor, new DownloadGpsTask(), td, i, n);
087            }
088        }
089        progressMonitor.addCancelListener(() -> {
090            for (DownloadTask dt : tasks) {
091                dt.cancel();
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<>(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 !primitive.incomplete)
122     * @param ds data set
123     *
124     * @return the set of ids of all complete, non-new primitives
125     */
126    protected Set<OsmPrimitive> getCompletePrimitives(DataSet ds) {
127        Set<OsmPrimitive> ret = new HashSet<>();
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<>();
144        for (OsmPrimitive primitive : potentiallyDeleted) {
145            if (primitive != null) {
146                toSelect.add(primitive);
147            }
148        }
149        EventQueue.invokeLater(() -> UpdateSelectionAction.updatePrimitives(toSelect));
150    }
151
152    /**
153     * Processes a set of primitives (given by a set of their ids) which might be deleted on the
154     * server. First prompts the user whether he wants to check the current state on the server. If
155     * yes, retrieves the current state on the server and checks whether the primitives are indeed
156     * deleted on the server.
157     *
158     * @param potentiallyDeleted a set of primitives (given by their ids)
159     */
160    protected void handlePotentiallyDeletedPrimitives(Set<OsmPrimitive> potentiallyDeleted) {
161        ButtonSpec[] options = new ButtonSpec[] {
162                new ButtonSpec(
163                        tr("Check on the server"),
164                        ImageProvider.get("ok"),
165                        tr("Click to check whether objects in your local dataset are deleted on the server"),
166                        null  /* no specific help topic */
167                        ),
168                        new ButtonSpec(
169                                tr("Ignore"),
170                                ImageProvider.get("cancel"),
171                                tr("Click to abort and to resume editing"),
172                                null /* no specific help topic */
173                                ),
174        };
175
176        String message = "<html>" + trn(
177                "There is {0} object in your local dataset which "
178                + "might be deleted on the server.<br>If you later try to delete or "
179                + "update this the server is likely to report a conflict.",
180                "There are {0} objects in your local dataset which "
181                + "might be deleted on the server.<br>If you later try to delete or "
182                + "update them the server is likely to report a conflict.",
183                potentiallyDeleted.size(), potentiallyDeleted.size())
184                + "<br>"
185                + trn("Click <strong>{0}</strong> to check the state of this object on the server.",
186                "Click <strong>{0}</strong> to check the state of these objects on the server.",
187                potentiallyDeleted.size(),
188                options[0].text) + "<br>"
189                + tr("Click <strong>{0}</strong> to ignore." + "</html>", options[1].text);
190
191        int ret = HelpAwareOptionPane.showOptionDialog(
192                Main.parent,
193                message,
194                tr("Deleted or moved objects"),
195                JOptionPane.WARNING_MESSAGE,
196                null,
197                options,
198                options[0],
199                ht("/Action/UpdateData#SyncPotentiallyDeletedObjects")
200                );
201        if (ret != 0 /* OK */)
202            return;
203
204        updatePotentiallyDeletedPrimitives(potentiallyDeleted);
205    }
206
207    /**
208     * Replies the set of primitive ids which have been downloaded by this task list
209     *
210     * @return the set of primitive ids which have been downloaded by this task list
211     */
212    public Set<OsmPrimitive> getDownloadedPrimitives() {
213        Set<OsmPrimitive> ret = new HashSet<>();
214        for (DownloadTask task : tasks) {
215            if (task instanceof DownloadOsmTask) {
216                DataSet ds = ((DownloadOsmTask) task).getDownloadedData();
217                if (ds != null) {
218                    ret.addAll(ds.allPrimitives());
219                }
220            }
221        }
222        return ret;
223    }
224
225    class PostDownloadProcessor implements Runnable {
226
227        private final boolean osmData;
228
229        PostDownloadProcessor(boolean osmData) {
230            this.osmData = osmData;
231        }
232
233        /**
234         * Grabs and displays the error messages after all download threads have finished.
235         */
236        @Override
237        public void run() {
238            progressMonitor.finishTask();
239
240            // wait for all download tasks to finish
241            //
242            for (Future<?> future : taskFutures) {
243                try {
244                    future.get();
245                } catch (InterruptedException | ExecutionException | CancellationException e) {
246                    Main.error(e);
247                    return;
248                }
249            }
250            Set<Object> errors = new LinkedHashSet<>();
251            for (DownloadTask dt : tasks) {
252                errors.addAll(dt.getErrorObjects());
253            }
254            if (!errors.isEmpty()) {
255                final Collection<String> items = new ArrayList<>();
256                for (Object error : errors) {
257                    if (error instanceof String) {
258                        items.add((String) error);
259                    } else if (error instanceof Exception) {
260                        items.add(ExceptionUtil.explainException((Exception) error));
261                    }
262                }
263
264                GuiHelper.runInEDT(() -> {
265                    if (items.size() == 1 && tr("No data found in this area.").equals(items.iterator().next())) {
266                        new Notification(items.iterator().next()).setIcon(JOptionPane.WARNING_MESSAGE).show();
267                    } else {
268                        JOptionPane.showMessageDialog(Main.parent, "<html>"
269                                + tr("The following errors occurred during mass download: {0}",
270                                        Utils.joinAsHtmlUnorderedList(items)) + "</html>",
271                                tr("Errors during download"), JOptionPane.ERROR_MESSAGE);
272                    }
273                });
274
275                return;
276            }
277
278            // FIXME: this is a hack. We assume that the user canceled the whole download if at
279            // least one task was canceled or if it failed
280            //
281            for (DownloadTask task : tasks) {
282                if (task instanceof AbstractDownloadTask) {
283                    AbstractDownloadTask<?> absTask = (AbstractDownloadTask<?>) task;
284                    if (absTask.isCanceled() || absTask.isFailed())
285                        return;
286                }
287            }
288            final OsmDataLayer editLayer = Main.getLayerManager().getEditLayer();
289            if (editLayer != null && osmData) {
290                final Set<OsmPrimitive> myPrimitives = getCompletePrimitives(editLayer.data);
291                for (DownloadTask task : tasks) {
292                    if (task instanceof DownloadOsmTask) {
293                        DataSet ds = ((DownloadOsmTask) task).getDownloadedData();
294                        if (ds != null) {
295                            // myPrimitives.removeAll(ds.allPrimitives()) will do the same job but much slower
296                            for (OsmPrimitive primitive: ds.allPrimitives()) {
297                                myPrimitives.remove(primitive);
298                            }
299                        }
300                    }
301                }
302                if (!myPrimitives.isEmpty()) {
303                    GuiHelper.runInEDT(() -> handlePotentiallyDeletedPrimitives(myPrimitives));
304                }
305            }
306        }
307    }
308}