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