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