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