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("data"), 045 marktr("layer"), 046 marktr("selection"), 047 marktr("conflict"), 048 marktr("download"), 049 marktr("problem"), 050 marktr("previous"), 051 marktr("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.recalculateCenterScale(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.recalculateCenterScale(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}