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