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.concurrent.TimeUnit;
018
019import javax.swing.JOptionPane;
020import javax.swing.event.ListSelectionListener;
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        DataSet dataSet = Main.getLayerManager().getEditDataSet();
076        if (dataSet == null) {
077            return;
078        }
079        Collection<OsmPrimitive> sel = dataSet.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        if (Main.getLayerManager().getActiveLayer() == null) {
224            return null;
225        }
226        List<Layer> layers = LayerListDialog.getInstance().getModel().getSelectedLayers();
227        if (layers.isEmpty())
228            return null;
229        return layers.get(0);
230    }
231
232    private BoundingXYVisitor getBoundingBox() {
233        BoundingXYVisitor v = "problem".equals(mode) ? new ValidatorBoundingXYVisitor() : new BoundingXYVisitor();
234
235        switch (mode) {
236        case "problem":
237            return modeProblem((ValidatorBoundingXYVisitor) v);
238        case "data":
239            return modeData(v);
240        case "layer":
241            return modeLayer(v);
242        case "selection":
243        case "conflict":
244            return modeSelectionOrConflict(v);
245        case "download":
246            return modeDownload(v);
247        default:
248            return v;
249        }
250    }
251
252    private static BoundingXYVisitor modeProblem(ValidatorBoundingXYVisitor v) {
253        TestError error = Main.map.validatorDialog.getSelectedError();
254        if (error == null)
255            return null;
256        v.visit(error);
257        if (v.getBounds() == null)
258            return null;
259        v.enlargeBoundingBox(Main.pref.getDouble("validator.zoom-enlarge-bbox", 0.0002));
260        return v;
261    }
262
263    private static BoundingXYVisitor modeData(BoundingXYVisitor v) {
264        for (Layer l : Main.getLayerManager().getLayers()) {
265            l.visitBoundingBox(v);
266        }
267        return v;
268    }
269
270    private BoundingXYVisitor modeLayer(BoundingXYVisitor v) {
271        // try to zoom to the first selected layer
272        Layer l = getFirstSelectedLayer();
273        if (l == null)
274            return null;
275        l.visitBoundingBox(v);
276        return v;
277    }
278
279    private BoundingXYVisitor modeSelectionOrConflict(BoundingXYVisitor v) {
280        Collection<OsmPrimitive> sel = new HashSet<>();
281        if ("selection".equals(mode)) {
282            DataSet dataSet = getLayerManager().getEditDataSet();
283            if (dataSet != null) {
284                sel = dataSet.getSelected();
285            }
286        } else {
287            Conflict<? extends OsmPrimitive> c = Main.map.conflictDialog.getSelectedConflict();
288            if (c != null) {
289                sel.add(c.getMy());
290            } else if (Main.map.conflictDialog.getConflicts() != null) {
291                sel = Main.map.conflictDialog.getConflicts().getMyConflictParties();
292            }
293        }
294        if (sel.isEmpty()) {
295            JOptionPane.showMessageDialog(
296                    Main.parent,
297                    "selection".equals(mode) ? tr("Nothing selected to zoom to.") : tr("No conflicts to zoom to"),
298                    tr("Information"),
299                    JOptionPane.INFORMATION_MESSAGE);
300            return null;
301        }
302        for (OsmPrimitive osm : sel) {
303            osm.accept(v);
304        }
305
306        // Increase the bounding box by up to 100% to give more context.
307        v.enlargeBoundingBoxLogarithmically(100);
308        // Make the bounding box at least 100 meter wide to
309        // ensure reasonable zoom level when zooming onto single nodes.
310        v.enlargeToMinSize(Main.pref.getDouble("zoom_to_selection_min_size_in_meter", 100));
311        return v;
312    }
313
314    private BoundingXYVisitor modeDownload(BoundingXYVisitor v) {
315        if (lastZoomTime > 0 &&
316                System.currentTimeMillis() - lastZoomTime > Main.pref.getLong("zoom.bounds.reset.time", TimeUnit.SECONDS.toMillis(10))) {
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 = e -> updateEnabledState();
413            } else if ("problem".equals(mode)) {
414                validatorSelectionListener = e -> updateEnabledState();
415            }
416        }
417
418        @Override
419        public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
420            if (conflictSelectionListener != null) {
421                if (newFrame != null) {
422                    newFrame.conflictDialog.addListSelectionListener(conflictSelectionListener);
423                } else if (oldFrame != null) {
424                    oldFrame.conflictDialog.removeListSelectionListener(conflictSelectionListener);
425                }
426            } else if (validatorSelectionListener != null) {
427                if (newFrame != null) {
428                    newFrame.validatorDialog.addTreeSelectionListener(validatorSelectionListener);
429                } else if (oldFrame != null) {
430                    oldFrame.validatorDialog.removeTreeSelectionListener(validatorSelectionListener);
431                }
432            }
433            updateEnabledState();
434        }
435    }
436}