001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.download;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Color;
007import java.awt.Dimension;
008import java.awt.Font;
009import java.awt.GridBagLayout;
010import java.util.ArrayList;
011import java.util.List;
012import java.util.concurrent.ExecutionException;
013import java.util.concurrent.Future;
014
015import javax.swing.Icon;
016import javax.swing.JCheckBox;
017import javax.swing.JLabel;
018import javax.swing.JOptionPane;
019import javax.swing.event.ChangeListener;
020
021import org.openstreetmap.josm.actions.downloadtasks.AbstractDownloadTask;
022import org.openstreetmap.josm.actions.downloadtasks.DownloadGpsTask;
023import org.openstreetmap.josm.actions.downloadtasks.DownloadNotesTask;
024import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTask;
025import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler;
026import org.openstreetmap.josm.data.Bounds;
027import org.openstreetmap.josm.data.ProjectionBounds;
028import org.openstreetmap.josm.data.ViewportData;
029import org.openstreetmap.josm.data.preferences.BooleanProperty;
030import org.openstreetmap.josm.gui.MainApplication;
031import org.openstreetmap.josm.gui.MapFrame;
032import org.openstreetmap.josm.gui.util.GuiHelper;
033import org.openstreetmap.josm.spi.preferences.Config;
034import org.openstreetmap.josm.tools.GBC;
035import org.openstreetmap.josm.tools.ImageProvider;
036import org.openstreetmap.josm.tools.Logging;
037import org.openstreetmap.josm.tools.Pair;
038
039/**
040 * Class defines the way data is fetched from the OSM server.
041 * @since 12652
042 */
043public class OSMDownloadSource implements DownloadSource<OSMDownloadSource.OSMDownloadData> {
044    /**
045     * The simple name for the {@link OSMDownloadSourcePanel}
046     * @since 12706
047     */
048    public static final String SIMPLE_NAME = "osmdownloadpanel";
049
050    @Override
051    public AbstractDownloadSourcePanel<OSMDownloadData> createPanel(DownloadDialog dialog) {
052        return new OSMDownloadSourcePanel(this, dialog);
053    }
054
055    @Override
056    public void doDownload(OSMDownloadData data, DownloadSettings settings) {
057        Bounds bbox = settings.getDownloadBounds()
058                .orElseThrow(() -> new IllegalArgumentException("OSM downloads requires bounds"));
059        boolean zoom = settings.zoomToData();
060        boolean newLayer = settings.asNewLayer();
061        List<Pair<AbstractDownloadTask<?>, Future<?>>> tasks = new ArrayList<>();
062
063        if (data.isDownloadOSMData()) {
064            DownloadOsmTask task = new DownloadOsmTask();
065            task.setZoomAfterDownload(zoom && !data.isDownloadGPX() && !data.isDownloadNotes());
066            Future<?> future = task.download(newLayer, bbox, null);
067            MainApplication.worker.submit(new PostDownloadHandler(task, future));
068            if (zoom) {
069                tasks.add(new Pair<>(task, future));
070            }
071        }
072
073        if (data.isDownloadGPX()) {
074            DownloadGpsTask task = new DownloadGpsTask();
075            task.setZoomAfterDownload(zoom && !data.isDownloadOSMData() && !data.isDownloadNotes());
076            Future<?> future = task.download(newLayer, bbox, null);
077            MainApplication.worker.submit(new PostDownloadHandler(task, future));
078            if (zoom) {
079                tasks.add(new Pair<>(task, future));
080            }
081        }
082
083        if (data.isDownloadNotes()) {
084            DownloadNotesTask task = new DownloadNotesTask();
085            task.setZoomAfterDownload(zoom && !data.isDownloadOSMData() && !data.isDownloadGPX());
086            Future<?> future = task.download(false, bbox, null);
087            MainApplication.worker.submit(new PostDownloadHandler(task, future));
088            if (zoom) {
089                tasks.add(new Pair<>(task, future));
090            }
091        }
092
093        if (zoom && tasks.size() > 1) {
094            MainApplication.worker.submit(() -> {
095                ProjectionBounds bounds = null;
096                // Wait for completion of download jobs
097                for (Pair<AbstractDownloadTask<?>, Future<?>> p : tasks) {
098                    try {
099                        p.b.get();
100                        ProjectionBounds b = p.a.getDownloadProjectionBounds();
101                        if (bounds == null) {
102                            bounds = b;
103                        } else if (b != null) {
104                            bounds.extend(b);
105                        }
106                    } catch (InterruptedException | ExecutionException ex) {
107                        Logging.warn(ex);
108                    }
109                }
110                MapFrame map = MainApplication.getMap();
111                // Zoom to the larger download bounds
112                if (map != null && bounds != null) {
113                    final ProjectionBounds pb = bounds;
114                    GuiHelper.runInEDTAndWait(() -> map.mapView.zoomTo(new ViewportData(pb)));
115                }
116            });
117        }
118    }
119
120    @Override
121    public String getLabel() {
122        return tr("Download from OSM");
123    }
124
125    @Override
126    public boolean onlyExpert() {
127        return false;
128    }
129
130    /**
131     * The GUI representation of the OSM download source.
132     * @since 12652
133     */
134    public static class OSMDownloadSourcePanel extends AbstractDownloadSourcePanel<OSMDownloadData> {
135
136        private final JCheckBox cbDownloadOsmData;
137        private final JCheckBox cbDownloadGpxData;
138        private final JCheckBox cbDownloadNotes;
139        private final JLabel sizeCheck = new JLabel();
140
141        private static final BooleanProperty DOWNLOAD_OSM = new BooleanProperty("download.osm.data", true);
142        private static final BooleanProperty DOWNLOAD_GPS = new BooleanProperty("download.osm.gps", false);
143        private static final BooleanProperty DOWNLOAD_NOTES = new BooleanProperty("download.osm.notes", false);
144
145        /**
146         * Creates a new {@link OSMDownloadSourcePanel}.
147         * @param dialog the parent download dialog, as {@code DownloadDialog.getInstance()} might not be initialized yet
148         * @param ds The osm download source the panel is for.
149         * @since 12900
150         */
151        public OSMDownloadSourcePanel(OSMDownloadSource ds, DownloadDialog dialog) {
152            super(ds);
153            setLayout(new GridBagLayout());
154
155            // size check depends on selected data source
156            final ChangeListener checkboxChangeListener = e ->
157                    dialog.getSelectedDownloadArea().ifPresent(this::updateSizeCheck);
158
159            // adding the download tasks
160            add(new JLabel(tr("Data Sources and Types:")), GBC.std().insets(5, 5, 1, 5).anchor(GBC.CENTER));
161            cbDownloadOsmData = new JCheckBox(tr("OpenStreetMap data"), true);
162            cbDownloadOsmData.setToolTipText(tr("Select to download OSM data in the selected download area."));
163            cbDownloadOsmData.getModel().addChangeListener(checkboxChangeListener);
164
165            cbDownloadGpxData = new JCheckBox(tr("Raw GPS data"));
166            cbDownloadGpxData.setToolTipText(tr("Select to download GPS traces in the selected download area."));
167            cbDownloadGpxData.getModel().addChangeListener(checkboxChangeListener);
168
169            cbDownloadNotes = new JCheckBox(tr("Notes"));
170            cbDownloadNotes.setToolTipText(tr("Select to download notes in the selected download area."));
171            cbDownloadNotes.getModel().addChangeListener(checkboxChangeListener);
172
173            Font labelFont = sizeCheck.getFont();
174            sizeCheck.setFont(labelFont.deriveFont(Font.PLAIN, labelFont.getSize()));
175
176            add(cbDownloadOsmData, GBC.std().insets(1, 5, 1, 5));
177            add(cbDownloadGpxData, GBC.std().insets(1, 5, 1, 5));
178            add(cbDownloadNotes, GBC.eol().insets(1, 5, 1, 5));
179            add(sizeCheck, GBC.eol().anchor(GBC.EAST).insets(5, 5, 5, 2));
180
181            setMinimumSize(new Dimension(450, 115));
182        }
183
184        @Override
185        public OSMDownloadData getData() {
186            return new OSMDownloadData(
187                    isDownloadOsmData(),
188                    isDownloadNotes(),
189                    isDownloadGpxData());
190        }
191
192        @Override
193        public void rememberSettings() {
194            DOWNLOAD_OSM.put(isDownloadOsmData());
195            DOWNLOAD_GPS.put(isDownloadGpxData());
196            DOWNLOAD_NOTES.put(isDownloadNotes());
197        }
198
199        @Override
200        public void restoreSettings() {
201            cbDownloadOsmData.setSelected(DOWNLOAD_OSM.get());
202            cbDownloadGpxData.setSelected(DOWNLOAD_GPS.get());
203            cbDownloadNotes.setSelected(DOWNLOAD_NOTES.get());
204        }
205
206        @Override
207        public boolean checkDownload(DownloadSettings settings) {
208            /*
209             * It is mandatory to specify the area to download from OSM.
210             */
211            if (!settings.getDownloadBounds().isPresent()) {
212                JOptionPane.showMessageDialog(
213                        this.getParent(),
214                        tr("Please select a download area first."),
215                        tr("Error"),
216                        JOptionPane.ERROR_MESSAGE
217                );
218
219                return false;
220            }
221
222            /*
223             * Checks if the user selected the type of data to download. At least one the following
224             * must be chosen : raw osm data, gpx data, notes.
225             * If none of those are selected, then the corresponding dialog is shown to inform the user.
226             */
227            if (!isDownloadOsmData() && !isDownloadGpxData() && !isDownloadNotes()) {
228                JOptionPane.showMessageDialog(
229                        this.getParent(),
230                        tr("<html>Neither <strong>{0}</strong> nor <strong>{1}</strong> nor <strong>{2}</strong> is enabled.<br>"
231                                        + "Please choose to either download OSM data, or GPX data, or Notes, or all.</html>",
232                                cbDownloadOsmData.getText(),
233                                cbDownloadGpxData.getText(),
234                                cbDownloadNotes.getText()
235                        ),
236                        tr("Error"),
237                        JOptionPane.ERROR_MESSAGE
238                );
239
240                return false;
241            }
242
243            this.rememberSettings();
244
245            return true;
246        }
247
248        /**
249         * Replies true if the user selected to download OSM data
250         *
251         * @return true if the user selected to download OSM data
252         */
253        public boolean isDownloadOsmData() {
254            return cbDownloadOsmData.isSelected();
255        }
256
257        /**
258         * Replies true if the user selected to download GPX data
259         *
260         * @return true if the user selected to download GPX data
261         */
262        public boolean isDownloadGpxData() {
263            return cbDownloadGpxData.isSelected();
264        }
265
266        /**
267         * Replies true if user selected to download notes
268         *
269         * @return true if user selected to download notes
270         */
271        public boolean isDownloadNotes() {
272            return cbDownloadNotes.isSelected();
273        }
274
275        @Override
276        public Icon getIcon() {
277            return ImageProvider.get("download");
278        }
279
280        @Override
281        public void boundingBoxChanged(Bounds bbox) {
282            updateSizeCheck(bbox);
283        }
284
285        @Override
286        public String getSimpleName() {
287            return SIMPLE_NAME;
288        }
289
290        private void updateSizeCheck(Bounds bbox) {
291            if (bbox == null) {
292                sizeCheck.setText(tr("No area selected yet"));
293                sizeCheck.setForeground(Color.darkGray);
294                return;
295            }
296
297            boolean isAreaTooLarge = false;
298            if (!isDownloadNotes() && !isDownloadOsmData() && !isDownloadGpxData()) {
299                isAreaTooLarge = false;
300            } else if (isDownloadNotes() && !isDownloadOsmData() && !isDownloadGpxData()) {
301                // see max_note_request_area in https://github.com/openstreetmap/openstreetmap-website/blob/master/config/example.application.yml
302                isAreaTooLarge = bbox.getArea() > Config.getPref().getDouble("osm-server.max-request-area-notes", 25);
303            } else {
304                // see max_request_area in https://github.com/openstreetmap/openstreetmap-website/blob/master/config/example.application.yml
305                isAreaTooLarge = bbox.getArea() > Config.getPref().getDouble("osm-server.max-request-area", 0.25);
306            }
307
308            displaySizeCheckResult(isAreaTooLarge);
309        }
310
311        private void displaySizeCheckResult(boolean isAreaTooLarge) {
312            if (isAreaTooLarge) {
313                sizeCheck.setText(tr("Download area too large; will probably be rejected by server"));
314                sizeCheck.setForeground(Color.red);
315            } else {
316                sizeCheck.setText(tr("Download area ok, size probably acceptable to server"));
317                sizeCheck.setForeground(Color.darkGray);
318            }
319        }
320
321    }
322
323    /**
324     * Encapsulates data that is required to download from the OSM server.
325     */
326    static class OSMDownloadData {
327        private final boolean downloadOSMData;
328        private final boolean downloadNotes;
329        private final boolean downloadGPX;
330
331        OSMDownloadData(boolean downloadOSMData, boolean downloadNotes, boolean downloadGPX) {
332            this.downloadOSMData = downloadOSMData;
333            this.downloadNotes = downloadNotes;
334            this.downloadGPX = downloadGPX;
335        }
336
337        boolean isDownloadOSMData() {
338            return downloadOSMData;
339        }
340
341        boolean isDownloadNotes() {
342            return downloadNotes;
343        }
344
345        boolean isDownloadGPX() {
346            return downloadGPX;
347        }
348    }
349}