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