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