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.LinkedHashSet;
014import java.util.LinkedList;
015import java.util.List;
016import java.util.Objects;
017import java.util.Set;
018import java.util.concurrent.CancellationException;
019import java.util.concurrent.ExecutionException;
020import java.util.concurrent.Future;
021import java.util.stream.Collectors;
022
023import javax.swing.JOptionPane;
024
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 final boolean zoomAfterDownload;
053    private ProgressMonitor progressMonitor;
054
055    /**
056     * Constructs a new {@code DownloadTaskList}. Zooms to each download area.
057     */
058    public DownloadTaskList() {
059        this(true);
060    }
061
062    /**
063     * Constructs a new {@code DownloadTaskList}.
064     * @param zoomAfterDownload whether to zoom to each download area
065     * @since 15205
066     */
067    public DownloadTaskList(boolean zoomAfterDownload) {
068        this.zoomAfterDownload = zoomAfterDownload;
069    }
070
071    private void addDownloadTask(ProgressMonitor progressMonitor, DownloadTask dt, Rectangle2D td, int i, int n) {
072        ProgressMonitor childProgress = progressMonitor.createSubTaskMonitor(1, false);
073        childProgress.setCustomText(tr("Download {0} of {1} ({2} left)", i, n, n - i));
074        dt.setZoomAfterDownload(zoomAfterDownload);
075        Future<?> future = dt.download(new DownloadParams(), new Bounds(td), childProgress);
076        taskFutures.add(future);
077        tasks.add(dt);
078    }
079
080    /**
081     * Downloads a list of areas from the OSM Server
082     * @param newLayer Set to true if all areas should be put into a single new layer
083     * @param rects The List of Rectangle2D to download
084     * @param osmData Set to true if OSM data should be downloaded
085     * @param gpxData Set to true if GPX data should be downloaded
086     * @param progressMonitor The progress monitor
087     * @return The Future representing the asynchronous download task
088     */
089    public Future<?> download(boolean newLayer, List<Rectangle2D> rects, boolean osmData, boolean gpxData, ProgressMonitor progressMonitor) {
090        this.progressMonitor = progressMonitor;
091        if (newLayer) {
092            Layer l = new OsmDataLayer(new DataSet(), OsmDataLayer.createNewName(), null);
093            MainApplication.getLayerManager().addLayer(l);
094            MainApplication.getLayerManager().setActiveLayer(l);
095        }
096
097        int n = (osmData && gpxData ? 2 : 1)*rects.size();
098        progressMonitor.beginTask(null, n);
099        int i = 0;
100        for (Rectangle2D td : rects) {
101            i++;
102            if (osmData) {
103                addDownloadTask(progressMonitor, new DownloadOsmTask(), td, i, n);
104            }
105            if (gpxData) {
106                addDownloadTask(progressMonitor, new DownloadGpsTask(), td, i, n);
107            }
108        }
109        progressMonitor.addCancelListener(() -> {
110            for (DownloadTask dt : tasks) {
111                dt.cancel();
112            }
113        });
114        return MainApplication.worker.submit(new PostDownloadProcessor(osmData));
115    }
116
117    /**
118     * Downloads a list of areas from the OSM Server
119     * @param newLayer Set to true if all areas should be put into a single new layer
120     * @param areas The Collection of Areas to download
121     * @param osmData Set to true if OSM data should be downloaded
122     * @param gpxData Set to true if GPX data should be downloaded
123     * @param progressMonitor The progress monitor
124     * @return The Future representing the asynchronous download task
125     */
126    public Future<?> download(boolean newLayer, Collection<Area> areas, boolean osmData, boolean gpxData, ProgressMonitor progressMonitor) {
127        progressMonitor.beginTask(tr("Updating data"));
128        try {
129            List<Rectangle2D> rects = areas.stream().map(Area::getBounds2D).collect(Collectors.toList());
130            return download(newLayer, rects, osmData, gpxData, progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
131        } finally {
132            progressMonitor.finishTask();
133        }
134    }
135
136    /**
137     * Replies the set of ids of all complete, non-new primitives (i.e. those with !primitive.incomplete)
138     * @param ds data set
139     *
140     * @return the set of ids of all complete, non-new primitives
141     */
142    protected Set<OsmPrimitive> getCompletePrimitives(DataSet ds) {
143        return ds.allPrimitives().stream().filter(p -> !p.isIncomplete() && !p.isNew()).collect(Collectors.toSet());
144    }
145
146    /**
147     * Updates the local state of a set of primitives (given by a set of primitive ids) with the
148     * state currently held on the server.
149     *
150     * @param potentiallyDeleted a set of ids to check update from the server
151     */
152    protected void updatePotentiallyDeletedPrimitives(Set<OsmPrimitive> potentiallyDeleted) {
153        final List<OsmPrimitive> toSelect = new ArrayList<>();
154        for (OsmPrimitive primitive : potentiallyDeleted) {
155            if (primitive != null) {
156                toSelect.add(primitive);
157            }
158        }
159        EventQueue.invokeLater(() -> UpdateSelectionAction.updatePrimitives(toSelect));
160    }
161
162    /**
163     * Processes a set of primitives (given by a set of their ids) which might be deleted on the
164     * server. First prompts the user whether he wants to check the current state on the server. If
165     * yes, retrieves the current state on the server and checks whether the primitives are indeed
166     * deleted on the server.
167     *
168     * @param potentiallyDeleted a set of primitives (given by their ids)
169     */
170    protected void handlePotentiallyDeletedPrimitives(Set<OsmPrimitive> potentiallyDeleted) {
171        ButtonSpec[] options = new ButtonSpec[] {
172                new ButtonSpec(
173                        tr("Check on the server"),
174                        new ImageProvider("ok"),
175                        tr("Click to check whether objects in your local dataset are deleted on the server"),
176                        null /* no specific help topic */),
177                new ButtonSpec(
178                        tr("Ignore"),
179                        new ImageProvider("cancel"),
180                        tr("Click to abort and to resume editing"),
181                        null /* no specific help topic */),
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                MainApplication.getMainFrame(),
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        return tasks.stream()
222                .filter(t -> t instanceof DownloadOsmTask)
223                .map(t -> ((DownloadOsmTask) t).getDownloadedData())
224                .filter(Objects::nonNull)
225                .flatMap(ds -> ds.allPrimitives().stream())
226                .collect(Collectors.toSet());
227    }
228
229    class PostDownloadProcessor implements Runnable {
230
231        private final boolean osmData;
232
233        PostDownloadProcessor(boolean osmData) {
234            this.osmData = osmData;
235        }
236
237        /**
238         * Grabs and displays the error messages after all download threads have finished.
239         */
240        @Override
241        public void run() {
242            progressMonitor.finishTask();
243
244            // wait for all download tasks to finish
245            //
246            for (Future<?> future : taskFutures) {
247                try {
248                    future.get();
249                } catch (InterruptedException | ExecutionException | CancellationException e) {
250                    Logging.error(e);
251                    return;
252                }
253            }
254            Set<Object> errors = new LinkedHashSet<>();
255            for (DownloadTask dt : tasks) {
256                errors.addAll(dt.getErrorObjects());
257            }
258            if (!errors.isEmpty()) {
259                final Collection<String> items = new ArrayList<>();
260                for (Object error : errors) {
261                    if (error instanceof String) {
262                        items.add((String) error);
263                    } else if (error instanceof Exception) {
264                        items.add(ExceptionUtil.explainException((Exception) error));
265                    }
266                }
267
268                GuiHelper.runInEDT(() -> {
269                    if (items.size() == 1 && tr("No data found in this area.").equals(items.iterator().next())) {
270                        new Notification(items.iterator().next()).setIcon(JOptionPane.WARNING_MESSAGE).show();
271                    } else {
272                        JOptionPane.showMessageDialog(MainApplication.getMainFrame(), "<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 = MainApplication.getLayerManager().getEditLayer();
293            if (editLayer != null && osmData) {
294                final Set<OsmPrimitive> myPrimitives = getCompletePrimitives(editLayer.getDataSet());
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(() -> handlePotentiallyDeletedPrimitives(myPrimitives));
308                }
309            }
310        }
311    }
312}