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;
017
018import javax.swing.JOptionPane;
019import javax.swing.event.ListSelectionEvent;
020import javax.swing.event.ListSelectionListener;
021import javax.swing.event.TreeSelectionEvent;
022import javax.swing.event.TreeSelectionListener;
023
024import org.openstreetmap.josm.Main;
025import org.openstreetmap.josm.data.Bounds;
026import org.openstreetmap.josm.data.DataSource;
027import org.openstreetmap.josm.data.conflict.Conflict;
028import org.openstreetmap.josm.data.osm.DataSet;
029import org.openstreetmap.josm.data.osm.OsmPrimitive;
030import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
031import org.openstreetmap.josm.data.validation.TestError;
032import org.openstreetmap.josm.gui.MapFrame;
033import org.openstreetmap.josm.gui.MapFrameListener;
034import org.openstreetmap.josm.gui.MapView;
035import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
036import org.openstreetmap.josm.gui.dialogs.ValidatorDialog.ValidatorBoundingXYVisitor;
037import org.openstreetmap.josm.gui.layer.Layer;
038import org.openstreetmap.josm.tools.Shortcut;
039
040/**
041 * Toggles the autoScale feature of the mapView
042 * @author imi
043 */
044public class AutoScaleAction extends JosmAction {
045
046    /**
047     * A list of things we can zoom to. The zoom target is given depending on the mode.
048     */
049    public static final Collection<String> MODES = Collections.unmodifiableList(Arrays.asList(
050        marktr(/* ICON(dialogs/autoscale/) */ "data"),
051        marktr(/* ICON(dialogs/autoscale/) */ "layer"),
052        marktr(/* ICON(dialogs/autoscale/) */ "selection"),
053        marktr(/* ICON(dialogs/autoscale/) */ "conflict"),
054        marktr(/* ICON(dialogs/autoscale/) */ "download"),
055        marktr(/* ICON(dialogs/autoscale/) */ "problem"),
056        marktr(/* ICON(dialogs/autoscale/) */ "previous"),
057        marktr(/* ICON(dialogs/autoscale/) */ "next")));
058
059    /**
060     * One of {@link #MODES}. Defines what we are zooming to.
061     */
062    private final String mode;
063
064    /** Time of last zoom to bounds action */
065    protected long lastZoomTime = -1;
066    /** Last zommed bounds */
067    protected int lastZoomArea = -1;
068
069    /**
070     * Zooms the current map view to the currently selected primitives.
071     * Does nothing if there either isn't a current map view or if there isn't a current data
072     * layer.
073     *
074     */
075    public static void zoomToSelection() {
076        DataSet dataSet = Main.getLayerManager().getEditDataSet();
077        if (dataSet == null) {
078            return;
079        }
080        Collection<OsmPrimitive> sel = dataSet.getSelected();
081        if (sel.isEmpty()) {
082            JOptionPane.showMessageDialog(
083                    Main.parent,
084                    tr("Nothing selected to zoom to."),
085                    tr("Information"),
086                    JOptionPane.INFORMATION_MESSAGE);
087            return;
088        }
089        zoomTo(sel);
090    }
091
092    /**
093     * Zooms the view to display the given set of primitives.
094     * @param sel The primitives to zoom to, e.g. the current selection.
095     */
096    public static void zoomTo(Collection<OsmPrimitive> sel) {
097        BoundingXYVisitor bboxCalculator = new BoundingXYVisitor();
098        bboxCalculator.computeBoundingBox(sel);
099        // increase bbox. This is required
100        // especially if the bbox contains one single node, but helpful
101        // in most other cases as well.
102        bboxCalculator.enlargeBoundingBox();
103        if (bboxCalculator.getBounds() != null) {
104            Main.map.mapView.zoomTo(bboxCalculator);
105        }
106    }
107
108    /**
109     * Performs the auto scale operation of the given mode without the need to create a new action.
110     * @param mode One of {@link #MODES}.
111     */
112    public static void autoScale(String mode) {
113        new AutoScaleAction(mode, false).autoScale();
114    }
115
116    private static int getModeShortcut(String mode) {
117        int shortcut = -1;
118
119        // TODO: convert this to switch/case and make sure the parsing still works
120        // CHECKSTYLE.OFF: LeftCurly
121        // CHECKSTYLE.OFF: RightCurly
122        /* leave as single line for shortcut overview parsing! */
123        if (mode.equals("data")) { shortcut = KeyEvent.VK_1; }
124        else if (mode.equals("layer")) { shortcut = KeyEvent.VK_2; }
125        else if (mode.equals("selection")) { shortcut = KeyEvent.VK_3; }
126        else if (mode.equals("conflict")) { shortcut = KeyEvent.VK_4; }
127        else if (mode.equals("download")) { shortcut = KeyEvent.VK_5; }
128        else if (mode.equals("problem")) { shortcut = KeyEvent.VK_6; }
129        else if (mode.equals("previous")) { shortcut = KeyEvent.VK_8; }
130        else if (mode.equals("next")) { shortcut = KeyEvent.VK_9; }
131        // CHECKSTYLE.ON: LeftCurly
132        // CHECKSTYLE.ON: RightCurly
133
134        return shortcut;
135    }
136
137    /**
138     * Constructs a new {@code AutoScaleAction}.
139     * @param mode The autoscale mode (one of {@link AutoScaleAction#MODES})
140     * @param marker Used only to differentiate from default constructor
141     */
142    private AutoScaleAction(String mode, boolean marker) {
143        super(false);
144        this.mode = mode;
145    }
146
147    /**
148     * Constructs a new {@code AutoScaleAction}.
149     * @param mode The autoscale mode (one of {@link AutoScaleAction#MODES})
150     */
151    public AutoScaleAction(final String mode) {
152        super(tr("Zoom to {0}", tr(mode)), "dialogs/autoscale/" + mode, tr("Zoom the view to {0}.", tr(mode)),
153                Shortcut.registerShortcut("view:zoom" + mode, tr("View: {0}", tr("Zoom to {0}", tr(mode))),
154                        getModeShortcut(mode), Shortcut.DIRECT), true, null, false);
155        String modeHelp = Character.toUpperCase(mode.charAt(0)) + mode.substring(1);
156        putValue("help", "Action/AutoScale/" + modeHelp);
157        this.mode = mode;
158        switch (mode) {
159        case "data":
160            putValue("help", ht("/Action/ZoomToData"));
161            break;
162        case "layer":
163            putValue("help", ht("/Action/ZoomToLayer"));
164            break;
165        case "selection":
166            putValue("help", ht("/Action/ZoomToSelection"));
167            break;
168        case "conflict":
169            putValue("help", ht("/Action/ZoomToConflict"));
170            break;
171        case "problem":
172            putValue("help", ht("/Action/ZoomToProblem"));
173            break;
174        case "download":
175            putValue("help", ht("/Action/ZoomToDownload"));
176            break;
177        case "previous":
178            putValue("help", ht("/Action/ZoomToPrevious"));
179            break;
180        case "next":
181            putValue("help", ht("/Action/ZoomToNext"));
182            break;
183        default:
184            throw new IllegalArgumentException("Unknown mode: " + mode);
185        }
186        installAdapters();
187    }
188
189    /**
190     * Performs this auto scale operation for the mode this action is in.
191     */
192    public void autoScale() {
193        if (Main.isDisplayingMapView()) {
194            switch (mode) {
195            case "previous":
196                Main.map.mapView.zoomPrevious();
197                break;
198            case "next":
199                Main.map.mapView.zoomNext();
200                break;
201            default:
202                BoundingXYVisitor bbox = getBoundingBox();
203                if (bbox != null && bbox.getBounds() != null) {
204                    Main.map.mapView.zoomTo(bbox);
205                }
206            }
207        }
208        putValue("active", Boolean.TRUE);
209    }
210
211    @Override
212    public void actionPerformed(ActionEvent e) {
213        autoScale();
214    }
215
216    /**
217     * Replies the first selected layer in the layer list dialog. null, if no
218     * such layer exists, either because the layer list dialog is not yet created
219     * or because no layer is selected.
220     *
221     * @return the first selected layer in the layer list dialog
222     */
223    protected Layer getFirstSelectedLayer() {
224        if (Main.getLayerManager().getActiveLayer() == null) {
225            return null;
226        }
227        List<Layer> layers = LayerListDialog.getInstance().getModel().getSelectedLayers();
228        if (layers.isEmpty())
229            return null;
230        return layers.get(0);
231    }
232
233    private BoundingXYVisitor getBoundingBox() {
234        BoundingXYVisitor v = "problem".equals(mode) ? new ValidatorBoundingXYVisitor() : new BoundingXYVisitor();
235
236        switch (mode) {
237        case "problem":
238            return modeProblem(v);
239        case "data":
240            return modeData(v);
241        case "layer":
242            return modeLayer(v);
243        case "selection":
244        case "conflict":
245            return modeSelectionOrConflict(v);
246        case "download":
247            return modeDownload(v);
248        default:
249            return v;
250        }
251    }
252
253    private static BoundingXYVisitor modeProblem(BoundingXYVisitor v) {
254        TestError error = Main.map.validatorDialog.getSelectedError();
255        if (error == null)
256            return null;
257        ((ValidatorBoundingXYVisitor) v).visit(error);
258        if (v.getBounds() == null)
259            return null;
260        v.enlargeBoundingBox(Main.pref.getDouble("validator.zoom-enlarge-bbox", 0.0002));
261        return v;
262    }
263
264    private static BoundingXYVisitor modeData(BoundingXYVisitor v) {
265        for (Layer l : Main.getLayerManager().getLayers()) {
266            l.visitBoundingBox(v);
267        }
268        return v;
269    }
270
271    private BoundingXYVisitor modeLayer(BoundingXYVisitor v) {
272        // try to zoom to the first selected layer
273        Layer l = getFirstSelectedLayer();
274        if (l == null)
275            return null;
276        l.visitBoundingBox(v);
277        return v;
278    }
279
280    private BoundingXYVisitor modeSelectionOrConflict(BoundingXYVisitor v) {
281        Collection<OsmPrimitive> sel = new HashSet<>();
282        if ("selection".equals(mode)) {
283            DataSet dataSet = getLayerManager().getEditDataSet();
284            if (dataSet != null) {
285                sel = dataSet.getSelected();
286            }
287        } else {
288            Conflict<? extends OsmPrimitive> c = Main.map.conflictDialog.getSelectedConflict();
289            if (c != null) {
290                sel.add(c.getMy());
291            } else if (Main.map.conflictDialog.getConflicts() != null) {
292                sel = Main.map.conflictDialog.getConflicts().getMyConflictParties();
293            }
294        }
295        if (sel.isEmpty()) {
296            JOptionPane.showMessageDialog(
297                    Main.parent,
298                    "selection".equals(mode) ? tr("Nothing selected to zoom to.") : tr("No conflicts to zoom to"),
299                    tr("Information"),
300                    JOptionPane.INFORMATION_MESSAGE);
301            return null;
302        }
303        for (OsmPrimitive osm : sel) {
304            osm.accept(v);
305        }
306
307        // Increase the bounding box by up to 100% to give more context.
308        v.enlargeBoundingBoxLogarithmically(100);
309        // Make the bounding box at least 100 meter wide to
310        // ensure reasonable zoom level when zooming onto single nodes.
311        v.enlargeToMinSize(Main.pref.getDouble("zoom_to_selection_min_size_in_meter", 100));
312        return v;
313    }
314
315    private BoundingXYVisitor modeDownload(BoundingXYVisitor v) {
316        if (lastZoomTime > 0 && System.currentTimeMillis() - lastZoomTime > Main.pref.getLong("zoom.bounds.reset.time", 10L*1000L)) {
317            lastZoomTime = -1;
318        }
319        final DataSet dataset = getLayerManager().getEditDataSet();
320        if (dataset != null) {
321            List<DataSource> dataSources = new ArrayList<>(dataset.getDataSources());
322            int s = dataSources.size();
323            if (s > 0) {
324                if (lastZoomTime == -1 || lastZoomArea == -1 || lastZoomArea > s) {
325                    lastZoomArea = s-1;
326                    v.visit(dataSources.get(lastZoomArea).bounds);
327                } else if (lastZoomArea > 0) {
328                    lastZoomArea -= 1;
329                    v.visit(dataSources.get(lastZoomArea).bounds);
330                } else {
331                    lastZoomArea = -1;
332                    Area sourceArea = Main.getLayerManager().getEditDataSet().getDataSourceArea();
333                    if (sourceArea != null) {
334                        v.visit(new Bounds(sourceArea.getBounds2D()));
335                    }
336                }
337                lastZoomTime = System.currentTimeMillis();
338            } else {
339                lastZoomTime = -1;
340                lastZoomArea = -1;
341            }
342        }
343        return v;
344    }
345
346    @Override
347    protected void updateEnabledState() {
348        DataSet ds = getLayerManager().getEditDataSet();
349        switch (mode) {
350        case "selection":
351            setEnabled(ds != null && !ds.selectionEmpty());
352            break;
353        case "layer":
354            setEnabled(getFirstSelectedLayer() != null);
355            break;
356        case "conflict":
357            setEnabled(Main.map != null && Main.map.conflictDialog.getSelectedConflict() != null);
358            break;
359        case "download":
360            setEnabled(ds != null && !ds.getDataSources().isEmpty());
361            break;
362        case "problem":
363            setEnabled(Main.map != null && Main.map.validatorDialog.getSelectedError() != null);
364            break;
365        case "previous":
366            setEnabled(Main.isDisplayingMapView() && Main.map.mapView.hasZoomUndoEntries());
367            break;
368        case "next":
369            setEnabled(Main.isDisplayingMapView() && Main.map.mapView.hasZoomRedoEntries());
370            break;
371        default:
372            setEnabled(!getLayerManager().getLayers().isEmpty());
373        }
374    }
375
376    @Override
377    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
378        if ("selection".equals(mode)) {
379            setEnabled(selection != null && !selection.isEmpty());
380        }
381    }
382
383    @Override
384    protected final void installAdapters() {
385        super.installAdapters();
386        // make this action listen to zoom and mapframe change events
387        //
388        MapView.addZoomChangeListener(new ZoomChangeAdapter());
389        Main.addMapFrameListener(new MapFrameAdapter());
390        initEnabledState();
391    }
392
393    /**
394     * Adapter for zoom change events
395     */
396    private class ZoomChangeAdapter implements MapView.ZoomChangeListener {
397        @Override
398        public void zoomChanged() {
399            updateEnabledState();
400        }
401    }
402
403    /**
404     * Adapter for MapFrame change events
405     */
406    private class MapFrameAdapter implements MapFrameListener {
407        private ListSelectionListener conflictSelectionListener;
408        private TreeSelectionListener validatorSelectionListener;
409
410        MapFrameAdapter() {
411            if ("conflict".equals(mode)) {
412                conflictSelectionListener = new ListSelectionListener() {
413                    @Override
414                    public void valueChanged(ListSelectionEvent e) {
415                        updateEnabledState();
416                    }
417                };
418            } else if ("problem".equals(mode)) {
419                validatorSelectionListener = new TreeSelectionListener() {
420                    @Override
421                    public void valueChanged(TreeSelectionEvent e) {
422                        updateEnabledState();
423                    }
424                };
425            }
426        }
427
428        @Override
429        public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
430            if (conflictSelectionListener != null) {
431                if (newFrame != null) {
432                    newFrame.conflictDialog.addListSelectionListener(conflictSelectionListener);
433                } else if (oldFrame != null) {
434                    oldFrame.conflictDialog.removeListSelectionListener(conflictSelectionListener);
435                }
436            } else if (validatorSelectionListener != null) {
437                if (newFrame != null) {
438                    newFrame.validatorDialog.addTreeSelectionListener(validatorSelectionListener);
439                } else if (oldFrame != null) {
440                    oldFrame.validatorDialog.removeTreeSelectionListener(validatorSelectionListener);
441                }
442            }
443            updateEnabledState();
444        }
445    }
446}