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