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