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