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}