001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.download;
003
004import static org.openstreetmap.josm.tools.I18n.marktr;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.awt.Color;
008import java.awt.Dimension;
009import java.awt.FlowLayout;
010import java.awt.Font;
011import java.awt.GridBagLayout;
012import java.lang.reflect.InvocationTargetException;
013import java.util.ArrayList;
014import java.util.Collection;
015import java.util.Collections;
016import java.util.List;
017import java.util.concurrent.ExecutionException;
018import java.util.concurrent.Future;
019
020import javax.swing.Box;
021import javax.swing.Icon;
022import javax.swing.JCheckBox;
023import javax.swing.JLabel;
024import javax.swing.JOptionPane;
025import javax.swing.JPanel;
026import javax.swing.event.ChangeListener;
027
028import org.openstreetmap.josm.actions.downloadtasks.AbstractDownloadTask;
029import org.openstreetmap.josm.actions.downloadtasks.DownloadGpsTask;
030import org.openstreetmap.josm.actions.downloadtasks.DownloadNotesTask;
031import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTask;
032import org.openstreetmap.josm.actions.downloadtasks.DownloadParams;
033import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler;
034import org.openstreetmap.josm.data.Bounds;
035import org.openstreetmap.josm.data.ProjectionBounds;
036import org.openstreetmap.josm.data.ViewportData;
037import org.openstreetmap.josm.data.gpx.GpxData;
038import org.openstreetmap.josm.data.osm.DataSet;
039import org.openstreetmap.josm.data.osm.NoteData;
040import org.openstreetmap.josm.data.preferences.BooleanProperty;
041import org.openstreetmap.josm.gui.MainApplication;
042import org.openstreetmap.josm.gui.MapFrame;
043import org.openstreetmap.josm.gui.util.GuiHelper;
044import org.openstreetmap.josm.spi.preferences.Config;
045import org.openstreetmap.josm.tools.GBC;
046import org.openstreetmap.josm.tools.ImageProvider;
047import org.openstreetmap.josm.tools.Logging;
048import org.openstreetmap.josm.tools.Pair;
049
050/**
051 * Class defines the way data is fetched from the OSM server.
052 * @since 12652
053 */
054public class OSMDownloadSource implements DownloadSource<List<IDownloadSourceType>> {
055    /**
056     * The simple name for the {@link OSMDownloadSourcePanel}
057     * @since 12706
058     */
059    public static final String SIMPLE_NAME = "osmdownloadpanel";
060
061    /** The possible methods to get data */
062    static final List<IDownloadSourceType> DOWNLOAD_SOURCES = new ArrayList<>();
063    static {
064        // Order is important (determines button order, and what gets zoomed to)
065        DOWNLOAD_SOURCES.add(new OsmDataDownloadType());
066        DOWNLOAD_SOURCES.add(new GpsDataDownloadType());
067        DOWNLOAD_SOURCES.add(new NotesDataDownloadType());
068    }
069
070    @Override
071    public AbstractDownloadSourcePanel<List<IDownloadSourceType>> createPanel(DownloadDialog dialog) {
072        return new OSMDownloadSourcePanel(this, dialog);
073    }
074
075    @Override
076    public void doDownload(List<IDownloadSourceType> data, DownloadSettings settings) {
077        Bounds bbox = settings.getDownloadBounds()
078                .orElseThrow(() -> new IllegalArgumentException("OSM downloads requires bounds"));
079        boolean zoom = settings.zoomToData();
080        boolean newLayer = settings.asNewLayer();
081        final List<Pair<AbstractDownloadTask<?>, Future<?>>> tasks = new ArrayList<>();
082        IDownloadSourceType zoomTask = zoom ? data.stream().findFirst().orElse(null) : null;
083        data.stream().filter(IDownloadSourceType::isEnabled).forEach(type -> {
084            try {
085                AbstractDownloadTask<?> task = type.getDownloadClass().getDeclaredConstructor().newInstance();
086                task.setZoomAfterDownload(type.equals(zoomTask));
087                Future<?> future = task.download(new DownloadParams().withNewLayer(newLayer), bbox, null);
088                MainApplication.worker.submit(new PostDownloadHandler(task, future));
089                if (zoom) {
090                    tasks.add(new Pair<AbstractDownloadTask<?>, Future<?>>(task, future));
091                }
092            } catch (InstantiationException | IllegalAccessException | IllegalArgumentException
093                    | InvocationTargetException | NoSuchMethodException | SecurityException e) {
094                Logging.error(e);
095            }
096        });
097
098        if (zoom && tasks.size() > 1) {
099            MainApplication.worker.submit(() -> {
100                ProjectionBounds bounds = null;
101                // Wait for completion of download jobs
102                for (Pair<AbstractDownloadTask<?>, Future<?>> p : tasks) {
103                    try {
104                        p.b.get();
105                        ProjectionBounds b = p.a.getDownloadProjectionBounds();
106                        if (bounds == null) {
107                            bounds = b;
108                        } else if (b != null) {
109                            bounds.extend(b);
110                        }
111                    } catch (InterruptedException | ExecutionException ex) {
112                        Logging.warn(ex);
113                    }
114                }
115                MapFrame map = MainApplication.getMap();
116                // Zoom to the larger download bounds
117                if (map != null && bounds != null) {
118                    final ProjectionBounds pb = bounds;
119                    GuiHelper.runInEDTAndWait(() -> map.mapView.zoomTo(new ViewportData(pb)));
120                }
121            });
122        }
123    }
124
125    @Override
126    public String getLabel() {
127        return tr("Download from OSM");
128    }
129
130    @Override
131    public boolean onlyExpert() {
132        return false;
133    }
134
135    /**
136     * Returns the possible downloads that JOSM can make in the default Download screen.
137     * @return The possible downloads that JOSM can make in the default Download screen
138     * @since 16503
139     */
140    public static List<IDownloadSourceType> getDownloadTypes() {
141        return Collections.unmodifiableList(DOWNLOAD_SOURCES);
142    }
143
144    /**
145     * Get the instance of a data download type
146     *
147     * @param <T> The type to get
148     * @param typeClazz The class of the type
149     * @return The type instance
150     * @since 16503
151     */
152    public static <T extends IDownloadSourceType> T getDownloadType(Class<T> typeClazz) {
153        return DOWNLOAD_SOURCES.stream().filter(typeClazz::isInstance).map(typeClazz::cast).findFirst().orElse(null);
154    }
155
156    /**
157     * Removes a download source type.
158     * @param type The IDownloadSourceType object to remove
159     * @return {@code true} if this download types contained the specified object
160     * @since 16503
161     */
162    public static boolean removeDownloadType(IDownloadSourceType type) {
163        if (type instanceof OsmDataDownloadType || type instanceof GpsDataDownloadType || type instanceof NotesDataDownloadType) {
164            throw new IllegalArgumentException(type.getClass().getName());
165        }
166        return DOWNLOAD_SOURCES.remove(type);
167    }
168
169    /**
170     * Add a download type to the default JOSM download window
171     *
172     * @param type The initialized type to download
173     * @return {@code true} (as specified by {@link Collection#add}), but it also returns false if the class already has an instance in the list
174     * @since 16503
175     */
176    public static boolean addDownloadType(IDownloadSourceType type) {
177        if (type instanceof OsmDataDownloadType || type instanceof GpsDataDownloadType || type instanceof NotesDataDownloadType) {
178            throw new IllegalArgumentException(type.getClass().getName());
179        } else if (getDownloadType(type.getClass()) != null) {
180            return false;
181        }
182        return DOWNLOAD_SOURCES.add(type);
183    }
184
185    /**
186     * The GUI representation of the OSM download source.
187     * @since 12652
188     */
189    public static class OSMDownloadSourcePanel extends AbstractDownloadSourcePanel<List<IDownloadSourceType>> {
190        private final JLabel sizeCheck = new JLabel();
191
192        /** This is used to keep track of the components for download sources, and to dynamically update/remove them */
193        private final JPanel downloadSourcesPanel;
194
195        private final ChangeListener checkboxChangeListener;
196
197        /**
198         * Label used in front of data types available for download. Made public for reuse in other download dialogs.
199         * @since 16155
200         */
201        public static final String DATA_SOURCES_AND_TYPES = marktr("Data Sources and Types:");
202
203        /**
204         * Creates a new {@link OSMDownloadSourcePanel}.
205         * @param dialog the parent download dialog, as {@code DownloadDialog.getInstance()} might not be initialized yet
206         * @param ds The osm download source the panel is for.
207         * @since 12900
208         */
209        public OSMDownloadSourcePanel(OSMDownloadSource ds, DownloadDialog dialog) {
210            super(ds);
211            setLayout(new GridBagLayout());
212
213            // size check depends on selected data source
214            checkboxChangeListener = e ->
215                    dialog.getSelectedDownloadArea().ifPresent(this::updateSizeCheck);
216
217            downloadSourcesPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
218            add(downloadSourcesPanel, GBC.eol().fill(GBC.HORIZONTAL));
219            updateSources();
220
221            sizeCheck.setFont(sizeCheck.getFont().deriveFont(Font.PLAIN));
222            JPanel sizeCheckPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
223            sizeCheckPanel.add(sizeCheck);
224            add(sizeCheckPanel, GBC.eol().fill(GBC.HORIZONTAL));
225
226            setMinimumSize(new Dimension(450, 115));
227        }
228
229        /**
230         * Update the source list for downloading data
231         */
232        protected void updateSources() {
233            downloadSourcesPanel.removeAll();
234            downloadSourcesPanel.add(new JLabel(tr(DATA_SOURCES_AND_TYPES)));
235            DOWNLOAD_SOURCES.forEach(obj -> {
236                final Icon icon = obj.getIcon();
237                if (icon != null) {
238                    downloadSourcesPanel.add(Box.createHorizontalStrut(6));
239                    downloadSourcesPanel.add(new JLabel(icon));
240                }
241                downloadSourcesPanel.add(obj.getCheckBox(checkboxChangeListener));
242            });
243        }
244
245        @Override
246        public List<IDownloadSourceType> getData() {
247            return DOWNLOAD_SOURCES;
248        }
249
250        @Override
251        public void rememberSettings() {
252            DOWNLOAD_SOURCES.forEach(type -> type.getBooleanProperty().put(type.getCheckBox().isSelected()));
253        }
254
255        @Override
256        public void restoreSettings() {
257            updateSources();
258            DOWNLOAD_SOURCES.forEach(type -> type.getCheckBox().setSelected(type.isEnabled()));
259        }
260
261        @Override
262        public void setVisible(boolean aFlag) {
263            super.setVisible(aFlag);
264            updateSources();
265        }
266
267        @Override
268        public boolean checkDownload(DownloadSettings settings) {
269            /*
270             * It is mandatory to specify the area to download from OSM.
271             */
272            if (!settings.getDownloadBounds().isPresent()) {
273                JOptionPane.showMessageDialog(
274                        this.getParent(),
275                        tr("Please select a download area first."),
276                        tr("Error"),
277                        JOptionPane.ERROR_MESSAGE
278                );
279
280                return false;
281            }
282
283            final Boolean slippyMapShowsDownloadBounds = settings.getSlippyMapBounds()
284                    .map(b -> b.intersects(settings.getDownloadBounds().get()))
285                    .orElse(true);
286            if (!slippyMapShowsDownloadBounds) {
287                final int confirmation = JOptionPane.showConfirmDialog(
288                        this.getParent(),
289                        tr("The slippy map no longer shows the selected download bounds. Continue?"),
290                        tr("Confirmation"),
291                        JOptionPane.OK_CANCEL_OPTION,
292                        JOptionPane.QUESTION_MESSAGE
293                );
294                if (confirmation != JOptionPane.OK_OPTION) {
295                    return false;
296                }
297            }
298
299            /*
300             * Checks if the user selected the type of data to download. At least one the following
301             * must be chosen : raw osm data, gpx data, notes.
302             * If none of those are selected, then the corresponding dialog is shown to inform the user.
303             */
304            if (DOWNLOAD_SOURCES.stream().noneMatch(IDownloadSourceType::isEnabled)) {
305                JOptionPane.showMessageDialog(
306                        this.getParent(),
307                        tr("Please select at least one download source."),
308                        tr("Error"),
309                        JOptionPane.ERROR_MESSAGE
310                );
311
312                return false;
313            }
314
315            this.rememberSettings();
316
317            return true;
318        }
319
320        @Override
321        public Icon getIcon() {
322            return ImageProvider.get("download");
323        }
324
325        @Override
326        public void boundingBoxChanged(Bounds bbox) {
327            updateSizeCheck(bbox);
328        }
329
330        @Override
331        public String getSimpleName() {
332            return SIMPLE_NAME;
333        }
334
335        private void updateSizeCheck(Bounds bbox) {
336            if (bbox == null) {
337                sizeCheck.setText(tr("No area selected yet"));
338                sizeCheck.setForeground(Color.darkGray);
339                return;
340            }
341
342            displaySizeCheckResult(DOWNLOAD_SOURCES.stream()
343                    .anyMatch(type -> type.isDownloadAreaTooLarge(bbox)));
344        }
345
346        private void displaySizeCheckResult(boolean isAreaTooLarge) {
347            if (isAreaTooLarge) {
348                sizeCheck.setText(tr("Download area too large; will probably be rejected by server"));
349                sizeCheck.setForeground(Color.red);
350            } else {
351                sizeCheck.setText(tr("Download area ok, size probably acceptable to server"));
352                sizeCheck.setForeground(Color.darkGray);
353            }
354        }
355    }
356
357    /**
358     * Encapsulates data that is required to download from the OSM server.
359     */
360    static class OSMDownloadData {
361
362        private final List<IDownloadSourceType> downloadPossibilities;
363
364        /**
365         * Constructs a new {@code OSMDownloadData}.
366         * @param downloadPossibilities A list of DataDownloadTypes (instantiated, with
367         *                              options set)
368         */
369        OSMDownloadData(List<IDownloadSourceType> downloadPossibilities) {
370            this.downloadPossibilities = downloadPossibilities;
371        }
372
373        /**
374         * Returns the download possibilities.
375         * @return A list of DataDownloadTypes (instantiated, with options set)
376         */
377        public List<IDownloadSourceType> getDownloadPossibilities() {
378            return downloadPossibilities;
379        }
380    }
381
382    private static class OsmDataDownloadType implements IDownloadSourceType {
383        static final BooleanProperty IS_ENABLED = new BooleanProperty("download.osm.data", true);
384        JCheckBox cbDownloadOsmData;
385
386        @Override
387        public JCheckBox getCheckBox(ChangeListener checkboxChangeListener) {
388            if (cbDownloadOsmData == null) {
389                cbDownloadOsmData = new JCheckBox(tr("OpenStreetMap data"), true);
390                cbDownloadOsmData.setToolTipText(tr("Select to download OSM data in the selected download area."));
391                cbDownloadOsmData.getModel().addChangeListener(checkboxChangeListener);
392            }
393            if (checkboxChangeListener != null) {
394                cbDownloadOsmData.getModel().addChangeListener(checkboxChangeListener);
395            }
396            return cbDownloadOsmData;
397        }
398
399        @Override
400        public Icon getIcon() {
401            return ImageProvider.get("layer/osmdata_small", ImageProvider.ImageSizes.SMALLICON);
402        }
403
404        @Override
405        public Class<? extends AbstractDownloadTask<DataSet>> getDownloadClass() {
406            return DownloadOsmTask.class;
407        }
408
409        @Override
410        public BooleanProperty getBooleanProperty() {
411            return IS_ENABLED;
412        }
413
414        @Override
415        public boolean isDownloadAreaTooLarge(Bounds bound) {
416            // see max_request_area in
417            // https://github.com/openstreetmap/openstreetmap-website/blob/master/config/example.application.yml
418            return bound.getArea() > Config.getPref().getDouble("osm-server.max-request-area", 0.25);
419        }
420    }
421
422    private static class GpsDataDownloadType implements IDownloadSourceType {
423        static final BooleanProperty IS_ENABLED = new BooleanProperty("download.osm.gps", false);
424        private JCheckBox cbDownloadGpxData;
425
426        @Override
427        public JCheckBox getCheckBox(ChangeListener checkboxChangeListener) {
428            if (cbDownloadGpxData == null) {
429                cbDownloadGpxData = new JCheckBox(tr("Raw GPS data"));
430                cbDownloadGpxData.setToolTipText(tr("Select to download GPS traces in the selected download area."));
431            }
432            if (checkboxChangeListener != null) {
433                cbDownloadGpxData.getModel().addChangeListener(checkboxChangeListener);
434            }
435
436            return cbDownloadGpxData;
437        }
438
439        @Override
440        public Icon getIcon() {
441            return ImageProvider.get("layer/gpx_small", ImageProvider.ImageSizes.SMALLICON);
442        }
443
444        @Override
445        public Class<? extends AbstractDownloadTask<GpxData>> getDownloadClass() {
446            return DownloadGpsTask.class;
447        }
448
449        @Override
450        public BooleanProperty getBooleanProperty() {
451            return IS_ENABLED;
452        }
453
454        @Override
455        public boolean isDownloadAreaTooLarge(Bounds bound) {
456            return false;
457        }
458    }
459
460    private static class NotesDataDownloadType implements IDownloadSourceType {
461        static final BooleanProperty IS_ENABLED = new BooleanProperty("download.osm.notes", false);
462        private JCheckBox cbDownloadNotes;
463
464        @Override
465        public JCheckBox getCheckBox(ChangeListener checkboxChangeListener) {
466            if (cbDownloadNotes == null) {
467                cbDownloadNotes = new JCheckBox(tr("Notes"));
468                cbDownloadNotes.setToolTipText(tr("Select to download notes in the selected download area."));
469            }
470            if (checkboxChangeListener != null) {
471                cbDownloadNotes.getModel().addChangeListener(checkboxChangeListener);
472            }
473
474            return cbDownloadNotes;
475        }
476
477        @Override
478        public Icon getIcon() {
479            return ImageProvider.get("dialogs/notes/note_open", ImageProvider.ImageSizes.SMALLICON);
480        }
481
482        @Override
483        public Class<? extends AbstractDownloadTask<NoteData>> getDownloadClass() {
484            return DownloadNotesTask.class;
485        }
486
487        @Override
488        public BooleanProperty getBooleanProperty() {
489            return IS_ENABLED;
490        }
491
492        @Override
493        public boolean isDownloadAreaTooLarge(Bounds bound) {
494            // see max_note_request_area in
495            // https://github.com/openstreetmap/openstreetmap-website/blob/master/config/example.application.yml
496            return bound.getArea() > Config.getPref().getDouble("osm-server.max-request-area-notes", 25);
497        }
498    }
499}