001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.marktr;
006import static org.openstreetmap.josm.tools.I18n.tr;
007
008import java.awt.event.ActionEvent;
009import java.awt.event.KeyEvent;
010import java.awt.geom.Area;
011import java.util.ArrayList;
012import java.util.Arrays;
013import java.util.Collection;
014import java.util.Collections;
015import java.util.HashSet;
016import java.util.List;
017import java.util.Objects;
018import java.util.concurrent.TimeUnit;
019
020import javax.swing.JOptionPane;
021import javax.swing.event.ListSelectionListener;
022import javax.swing.event.TreeSelectionListener;
023
024import org.openstreetmap.josm.data.Bounds;
025import org.openstreetmap.josm.data.DataSource;
026import org.openstreetmap.josm.data.conflict.Conflict;
027import org.openstreetmap.josm.data.osm.DataSet;
028import org.openstreetmap.josm.data.osm.IPrimitive;
029import org.openstreetmap.josm.data.osm.OsmData;
030import org.openstreetmap.josm.data.osm.OsmPrimitive;
031import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
032import org.openstreetmap.josm.data.validation.TestError;
033import org.openstreetmap.josm.gui.MainApplication;
034import org.openstreetmap.josm.gui.MapFrame;
035import org.openstreetmap.josm.gui.MapFrameListener;
036import org.openstreetmap.josm.gui.MapView;
037import org.openstreetmap.josm.gui.dialogs.ConflictDialog;
038import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
039import org.openstreetmap.josm.gui.dialogs.ValidatorDialog.ValidatorBoundingXYVisitor;
040import org.openstreetmap.josm.gui.layer.Layer;
041import org.openstreetmap.josm.spi.preferences.Config;
042import org.openstreetmap.josm.tools.Logging;
043import org.openstreetmap.josm.tools.Shortcut;
044
045/**
046 * Toggles the autoScale feature of the mapView
047 * @author imi
048 * @since 17
049 */
050public class AutoScaleAction extends JosmAction {
051
052    /**
053     * A list of things we can zoom to. The zoom target is given depending on the mode.
054     * @since 14221
055     */
056    public enum AutoScaleMode {
057        /** Zoom the window so that all the data fills the window area */
058        DATA(marktr(/* ICON(dialogs/autoscale/) */ "data")),
059        /** Zoom the window so that all the data on the currently selected layer fills the window area */
060        LAYER(marktr(/* ICON(dialogs/autoscale/) */ "layer")),
061        /** Zoom the window so that only data which is currently selected fills the window area */
062        SELECTION(marktr(/* ICON(dialogs/autoscale/) */ "selection")),
063        /** Zoom to the first selected conflict */
064        CONFLICT(marktr(/* ICON(dialogs/autoscale/) */ "conflict")),
065        /** Zoom the view to last downloaded data */
066        DOWNLOAD(marktr(/* ICON(dialogs/autoscale/) */ "download")),
067        /** Zoom the view to problem */
068        PROBLEM(marktr(/* ICON(dialogs/autoscale/) */ "problem")),
069        /** Zoom to the previous zoomed to scale and location (zoom undo) */
070        PREVIOUS(marktr(/* ICON(dialogs/autoscale/) */ "previous")),
071        /** Zoom to the next zoomed to scale and location (zoom redo) */
072        NEXT(marktr(/* ICON(dialogs/autoscale/) */ "next"));
073
074        private final String label;
075
076        AutoScaleMode(String label) {
077            this.label = label;
078        }
079
080        /**
081         * Returns the English label. Used for retrieving icons.
082         * @return the English label
083         */
084        public String getEnglishLabel() {
085            return label;
086        }
087
088        /**
089         * Returns the localized label. Used for display
090         * @return the localized label
091         */
092        public String getLocalizedLabel() {
093            return tr(label);
094        }
095
096        /**
097         * Returns {@code AutoScaleMode} for a given English label
098         * @param englishLabel English label
099         * @return {@code AutoScaleMode} for given English label
100         * @throws IllegalArgumentException if Engligh label is unknown
101         */
102        public static AutoScaleMode of(String englishLabel) {
103            for (AutoScaleMode v : values()) {
104                if (Objects.equals(v.label, englishLabel)) {
105                    return v;
106                }
107            }
108            throw new IllegalArgumentException(englishLabel);
109        }
110    }
111
112    /**
113     * A list of things we can zoom to. The zoom target is given depending on the mode.
114     * @deprecated Use {@link AutoScaleMode} enum instead
115     */
116    @Deprecated
117    public static final Collection<String> MODES = Collections.unmodifiableList(Arrays.asList(
118        marktr(/* ICON(dialogs/autoscale/) */ "data"),
119        marktr(/* ICON(dialogs/autoscale/) */ "layer"),
120        marktr(/* ICON(dialogs/autoscale/) */ "selection"),
121        marktr(/* ICON(dialogs/autoscale/) */ "conflict"),
122        marktr(/* ICON(dialogs/autoscale/) */ "download"),
123        marktr(/* ICON(dialogs/autoscale/) */ "problem"),
124        marktr(/* ICON(dialogs/autoscale/) */ "previous"),
125        marktr(/* ICON(dialogs/autoscale/) */ "next")));
126
127    /**
128     * One of {@link AutoScaleMode}. Defines what we are zooming to.
129     */
130    private final AutoScaleMode mode;
131
132    /** Time of last zoom to bounds action */
133    protected long lastZoomTime = -1;
134    /** Last zommed bounds */
135    protected int lastZoomArea = -1;
136
137    /**
138     * Zooms the current map view to the currently selected primitives.
139     * Does nothing if there either isn't a current map view or if there isn't a current data layer.
140     *
141     */
142    public static void zoomToSelection() {
143        OsmData<?, ?, ?, ?> dataSet = MainApplication.getLayerManager().getActiveData();
144        if (dataSet == null) {
145            return;
146        }
147        Collection<? extends IPrimitive> sel = dataSet.getSelected();
148        if (sel.isEmpty()) {
149            JOptionPane.showMessageDialog(
150                    MainApplication.getMainFrame(),
151                    tr("Nothing selected to zoom to."),
152                    tr("Information"),
153                    JOptionPane.INFORMATION_MESSAGE);
154            return;
155        }
156        zoomTo(sel);
157    }
158
159    /**
160     * Zooms the view to display the given set of primitives.
161     * @param sel The primitives to zoom to, e.g. the current selection.
162     */
163    public static void zoomTo(Collection<? extends IPrimitive> sel) {
164        BoundingXYVisitor bboxCalculator = new BoundingXYVisitor();
165        bboxCalculator.computeBoundingBox(sel);
166        if (bboxCalculator.getBounds() != null) {
167            MainApplication.getMap().mapView.zoomTo(bboxCalculator);
168        }
169    }
170
171    /**
172     * Performs the auto scale operation of the given mode without the need to create a new action.
173     * @param mode One of {@link #MODES}.
174     * @since 14221
175     */
176    public static void autoScale(AutoScaleMode mode) {
177        new AutoScaleAction(mode, false).autoScale();
178    }
179
180    /**
181     * Performs the auto scale operation of the given mode without the need to create a new action.
182     * @param mode One of {@link #MODES}.
183     * @deprecated Use {@link #autoScale(AutoScaleMode)} instead
184     */
185    @Deprecated
186    public static void autoScale(String mode) {
187        autoScale(AutoScaleMode.of(mode));
188    }
189
190    private static int getModeShortcut(String mode) {
191        int shortcut = -1;
192
193        // TODO: convert this to switch/case and make sure the parsing still works
194        // CHECKSTYLE.OFF: LeftCurly
195        // CHECKSTYLE.OFF: RightCurly
196        /* leave as single line for shortcut overview parsing! */
197        if (mode.equals("data")) { shortcut = KeyEvent.VK_1; }
198        else if (mode.equals("layer")) { shortcut = KeyEvent.VK_2; }
199        else if (mode.equals("selection")) { shortcut = KeyEvent.VK_3; }
200        else if (mode.equals("conflict")) { shortcut = KeyEvent.VK_4; }
201        else if (mode.equals("download")) { shortcut = KeyEvent.VK_5; }
202        else if (mode.equals("problem")) { shortcut = KeyEvent.VK_6; }
203        else if (mode.equals("previous")) { shortcut = KeyEvent.VK_8; }
204        else if (mode.equals("next")) { shortcut = KeyEvent.VK_9; }
205        // CHECKSTYLE.ON: LeftCurly
206        // CHECKSTYLE.ON: RightCurly
207
208        return shortcut;
209    }
210
211    /**
212     * Constructs a new {@code AutoScaleAction}.
213     * @param mode The autoscale mode (one of {@link AutoScaleMode})
214     * @param marker Must be set to false. Used only to differentiate from default constructor
215     */
216    private AutoScaleAction(AutoScaleMode mode, boolean marker) {
217        super(marker);
218        this.mode = mode;
219    }
220
221    /**
222     * Constructs a new {@code AutoScaleAction}.
223     * @param mode The autoscale mode (one of {@link AutoScaleAction#MODES})
224     * @deprecated Use {@link #AutoScaleAction(AutoScaleMode)} instead
225     */
226    @Deprecated
227    public AutoScaleAction(final String mode) {
228        this(AutoScaleMode.of(mode));
229    }
230
231    /**
232     * Constructs a new {@code AutoScaleAction}.
233     * @param mode The autoscale mode (one of {@link AutoScaleAction#MODES})
234     * @since 14221
235     */
236    public AutoScaleAction(final AutoScaleMode mode) {
237        super(tr("Zoom to {0}", mode.getLocalizedLabel()), "dialogs/autoscale/" + mode.getEnglishLabel(),
238              tr("Zoom the view to {0}.", mode.getLocalizedLabel()),
239              Shortcut.registerShortcut("view:zoom" + mode.getEnglishLabel(),
240                        tr("View: {0}", tr("Zoom to {0}", mode.getLocalizedLabel())),
241                        getModeShortcut(mode.getEnglishLabel()), Shortcut.DIRECT), true, null, false);
242        String label = mode.getEnglishLabel();
243        String modeHelp = Character.toUpperCase(label.charAt(0)) + label.substring(1);
244        setHelpId("Action/AutoScale/" + modeHelp);
245        this.mode = mode;
246        switch (mode) {
247        case DATA:
248            setHelpId(ht("/Action/ZoomToData"));
249            break;
250        case LAYER:
251            setHelpId(ht("/Action/ZoomToLayer"));
252            break;
253        case SELECTION:
254            setHelpId(ht("/Action/ZoomToSelection"));
255            break;
256        case CONFLICT:
257            setHelpId(ht("/Action/ZoomToConflict"));
258            break;
259        case PROBLEM:
260            setHelpId(ht("/Action/ZoomToProblem"));
261            break;
262        case DOWNLOAD:
263            setHelpId(ht("/Action/ZoomToDownload"));
264            break;
265        case PREVIOUS:
266            setHelpId(ht("/Action/ZoomToPrevious"));
267            break;
268        case NEXT:
269            setHelpId(ht("/Action/ZoomToNext"));
270            break;
271        default:
272            throw new IllegalArgumentException("Unknown mode: " + mode);
273        }
274        installAdapters();
275    }
276
277    /**
278     * Performs this auto scale operation for the mode this action is in.
279     */
280    public void autoScale() {
281        if (MainApplication.isDisplayingMapView()) {
282            MapView mapView = MainApplication.getMap().mapView;
283            switch (mode) {
284            case PREVIOUS:
285                mapView.zoomPrevious();
286                break;
287            case NEXT:
288                mapView.zoomNext();
289                break;
290            case PROBLEM:
291                modeProblem(new ValidatorBoundingXYVisitor());
292                break;
293            case DATA:
294                modeData(new BoundingXYVisitor());
295                break;
296            case LAYER:
297                modeLayer(new BoundingXYVisitor());
298                break;
299            case SELECTION:
300            case CONFLICT:
301                modeSelectionOrConflict(new BoundingXYVisitor());
302                break;
303            case DOWNLOAD:
304                modeDownload(new BoundingXYVisitor());
305                break;
306            }
307            putValue("active", Boolean.TRUE);
308        }
309    }
310
311    @Override
312    public void actionPerformed(ActionEvent e) {
313        autoScale();
314    }
315
316    /**
317     * Replies the first selected layer in the layer list dialog. null, if no
318     * such layer exists, either because the layer list dialog is not yet created
319     * or because no layer is selected.
320     *
321     * @return the first selected layer in the layer list dialog
322     */
323    protected Layer getFirstSelectedLayer() {
324        if (getLayerManager().getActiveLayer() == null) {
325            return null;
326        }
327        try {
328            List<Layer> layers = LayerListDialog.getInstance().getModel().getSelectedLayers();
329            if (!layers.isEmpty())
330                return layers.get(0);
331        } catch (IllegalStateException e) {
332            Logging.error(e);
333        }
334        return null;
335    }
336
337    private static void modeProblem(ValidatorBoundingXYVisitor v) {
338        TestError error = MainApplication.getMap().validatorDialog.getSelectedError();
339        if (error == null)
340            return;
341        v.visit(error);
342        if (v.getBounds() == null)
343            return;
344        MainApplication.getMap().mapView.zoomTo(v);
345    }
346
347    private static void modeData(BoundingXYVisitor v) {
348        for (Layer l : MainApplication.getLayerManager().getLayers()) {
349            l.visitBoundingBox(v);
350        }
351        MainApplication.getMap().mapView.zoomTo(v);
352    }
353
354    private void modeLayer(BoundingXYVisitor v) {
355        // try to zoom to the first selected layer
356        Layer l = getFirstSelectedLayer();
357        if (l == null)
358            return;
359        l.visitBoundingBox(v);
360        MainApplication.getMap().mapView.zoomTo(v);
361    }
362
363    private void modeSelectionOrConflict(BoundingXYVisitor v) {
364        Collection<IPrimitive> sel = new HashSet<>();
365        if (AutoScaleMode.SELECTION == mode) {
366            OsmData<?, ?, ?, ?> dataSet = getLayerManager().getActiveData();
367            if (dataSet != null) {
368                sel.addAll(dataSet.getSelected());
369            }
370        } else {
371            ConflictDialog conflictDialog = MainApplication.getMap().conflictDialog;
372            Conflict<? extends IPrimitive> c = conflictDialog.getSelectedConflict();
373            if (c != null) {
374                sel.add(c.getMy());
375            } else if (conflictDialog.getConflicts() != null) {
376                sel.addAll(conflictDialog.getConflicts().getMyConflictParties());
377            }
378        }
379        if (sel.isEmpty()) {
380            JOptionPane.showMessageDialog(
381                    MainApplication.getMainFrame(),
382                    AutoScaleMode.SELECTION == mode ? tr("Nothing selected to zoom to.") : tr("No conflicts to zoom to"),
383                    tr("Information"),
384                    JOptionPane.INFORMATION_MESSAGE);
385            return;
386        }
387        for (IPrimitive osm : sel) {
388            osm.accept(v);
389        }
390        if (v.getBounds() == null) {
391            return;
392        }
393
394        MainApplication.getMap().mapView.zoomTo(v);
395    }
396
397    private void modeDownload(BoundingXYVisitor v) {
398        if (lastZoomTime > 0 &&
399                System.currentTimeMillis() - lastZoomTime > Config.getPref().getLong("zoom.bounds.reset.time", TimeUnit.SECONDS.toMillis(10))) {
400            lastZoomTime = -1;
401        }
402        final DataSet dataset = getLayerManager().getActiveDataSet();
403        if (dataset != null) {
404            List<DataSource> dataSources = new ArrayList<>(dataset.getDataSources());
405            int s = dataSources.size();
406            if (s > 0) {
407                if (lastZoomTime == -1 || lastZoomArea == -1 || lastZoomArea > s) {
408                    lastZoomArea = s-1;
409                    v.visit(dataSources.get(lastZoomArea).bounds);
410                } else if (lastZoomArea > 0) {
411                    lastZoomArea -= 1;
412                    v.visit(dataSources.get(lastZoomArea).bounds);
413                } else {
414                    lastZoomArea = -1;
415                    Area sourceArea = getLayerManager().getActiveDataSet().getDataSourceArea();
416                    if (sourceArea != null) {
417                        v.visit(new Bounds(sourceArea.getBounds2D()));
418                    }
419                }
420                lastZoomTime = System.currentTimeMillis();
421            } else {
422                lastZoomTime = -1;
423                lastZoomArea = -1;
424            }
425        }
426        MainApplication.getMap().mapView.zoomTo(v);
427    }
428
429    @Override
430    protected void updateEnabledState() {
431        OsmData<?, ?, ?, ?> ds = getLayerManager().getActiveData();
432        MapFrame map = MainApplication.getMap();
433        switch (mode) {
434        case SELECTION:
435            setEnabled(ds != null && !ds.selectionEmpty());
436            break;
437        case LAYER:
438            setEnabled(getFirstSelectedLayer() != null);
439            break;
440        case CONFLICT:
441            setEnabled(map != null && map.conflictDialog.getSelectedConflict() != null);
442            break;
443        case DOWNLOAD:
444            setEnabled(ds != null && !ds.getDataSources().isEmpty());
445            break;
446        case PROBLEM:
447            setEnabled(map != null && map.validatorDialog.getSelectedError() != null);
448            break;
449        case PREVIOUS:
450            setEnabled(MainApplication.isDisplayingMapView() && map.mapView.hasZoomUndoEntries());
451            break;
452        case NEXT:
453            setEnabled(MainApplication.isDisplayingMapView() && map.mapView.hasZoomRedoEntries());
454            break;
455        default:
456            setEnabled(!getLayerManager().getLayers().isEmpty());
457        }
458    }
459
460    @Override
461    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
462        if (AutoScaleMode.SELECTION == mode) {
463            setEnabled(selection != null && !selection.isEmpty());
464        }
465    }
466
467    @Override
468    protected final void installAdapters() {
469        super.installAdapters();
470        // make this action listen to zoom and mapframe change events
471        //
472        MapView.addZoomChangeListener(new ZoomChangeAdapter());
473        MainApplication.addMapFrameListener(new MapFrameAdapter());
474        initEnabledState();
475    }
476
477    /**
478     * Adapter for zoom change events
479     */
480    private class ZoomChangeAdapter implements MapView.ZoomChangeListener {
481        @Override
482        public void zoomChanged() {
483            updateEnabledState();
484        }
485    }
486
487    /**
488     * Adapter for MapFrame change events
489     */
490    private class MapFrameAdapter implements MapFrameListener {
491        private ListSelectionListener conflictSelectionListener;
492        private TreeSelectionListener validatorSelectionListener;
493
494        MapFrameAdapter() {
495            if (AutoScaleMode.CONFLICT == mode) {
496                conflictSelectionListener = e -> updateEnabledState();
497            } else if (AutoScaleMode.PROBLEM == mode) {
498                validatorSelectionListener = e -> updateEnabledState();
499            }
500        }
501
502        @Override
503        public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
504            if (conflictSelectionListener != null) {
505                if (newFrame != null) {
506                    newFrame.conflictDialog.addListSelectionListener(conflictSelectionListener);
507                } else if (oldFrame != null) {
508                    oldFrame.conflictDialog.removeListSelectionListener(conflictSelectionListener);
509                }
510            } else if (validatorSelectionListener != null) {
511                if (newFrame != null) {
512                    newFrame.validatorDialog.addTreeSelectionListener(validatorSelectionListener);
513                } else if (oldFrame != null) {
514                    oldFrame.validatorDialog.removeTreeSelectionListener(validatorSelectionListener);
515                }
516            }
517            updateEnabledState();
518        }
519    }
520}