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