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.ArrayList; 011import java.util.Arrays; 012import java.util.Collection; 013import java.util.Collections; 014import java.util.HashSet; 015import java.util.List; 016 017import javax.swing.JOptionPane; 018import javax.swing.event.ListSelectionEvent; 019import javax.swing.event.ListSelectionListener; 020import javax.swing.event.TreeSelectionEvent; 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 if (Main.main == null || !Main.main.hasEditLayer()) 076 return; 077 Collection<OsmPrimitive> sel = Main.main.getEditLayer().data.getSelected(); 078 if (sel.isEmpty()) { 079 JOptionPane.showMessageDialog( 080 Main.parent, 081 tr("Nothing selected to zoom to."), 082 tr("Information"), 083 JOptionPane.INFORMATION_MESSAGE); 084 return; 085 } 086 zoomTo(sel); 087 } 088 089 /** 090 * Zooms the view to display the given set of primitives. 091 * @param sel The primitives to zoom to, e.g. the current selection. 092 */ 093 public static void zoomTo(Collection<OsmPrimitive> sel) { 094 BoundingXYVisitor bboxCalculator = new BoundingXYVisitor(); 095 bboxCalculator.computeBoundingBox(sel); 096 // increase bbox. This is required 097 // especially if the bbox contains one single node, but helpful 098 // in most other cases as well. 099 bboxCalculator.enlargeBoundingBox(); 100 if (bboxCalculator.getBounds() != null) { 101 Main.map.mapView.zoomTo(bboxCalculator); 102 } 103 } 104 105 /** 106 * Performs the auto scale operation of the given mode without the need to create a new action. 107 * @param mode One of {@link #MODES}. 108 */ 109 public static void autoScale(String mode) { 110 new AutoScaleAction(mode, false).autoScale(); 111 } 112 113 private static int getModeShortcut(String mode) { 114 int shortcut = -1; 115 116 // TODO: convert this to switch/case and make sure the parsing still works 117 // CHECKSTYLE.OFF: LeftCurly 118 // CHECKSTYLE.OFF: RightCurly 119 /* leave as single line for shortcut overview parsing! */ 120 if (mode.equals("data")) { shortcut = KeyEvent.VK_1; } 121 else if (mode.equals("layer")) { shortcut = KeyEvent.VK_2; } 122 else if (mode.equals("selection")) { shortcut = KeyEvent.VK_3; } 123 else if (mode.equals("conflict")) { shortcut = KeyEvent.VK_4; } 124 else if (mode.equals("download")) { shortcut = KeyEvent.VK_5; } 125 else if (mode.equals("problem")) { shortcut = KeyEvent.VK_6; } 126 else if (mode.equals("previous")) { shortcut = KeyEvent.VK_8; } 127 else if (mode.equals("next")) { shortcut = KeyEvent.VK_9; } 128 // CHECKSTYLE.ON: LeftCurly 129 // CHECKSTYLE.ON: RightCurly 130 131 return shortcut; 132 } 133 134 /** 135 * Constructs a new {@code AutoScaleAction}. 136 * @param mode The autoscale mode (one of {@link AutoScaleAction#MODES}) 137 * @param marker Used only to differentiate from default constructor 138 */ 139 private AutoScaleAction(String mode, boolean marker) { 140 super(false); 141 this.mode = mode; 142 } 143 144 /** 145 * Constructs a new {@code AutoScaleAction}. 146 * @param mode The autoscale mode (one of {@link AutoScaleAction#MODES}) 147 */ 148 public AutoScaleAction(final String mode) { 149 super(tr("Zoom to {0}", tr(mode)), "dialogs/autoscale/" + mode, tr("Zoom the view to {0}.", tr(mode)), 150 Shortcut.registerShortcut("view:zoom" + mode, tr("View: {0}", tr("Zoom to {0}", tr(mode))), 151 getModeShortcut(mode), Shortcut.DIRECT), true, null, false); 152 String modeHelp = Character.toUpperCase(mode.charAt(0)) + mode.substring(1); 153 putValue("help", "Action/AutoScale/" + modeHelp); 154 this.mode = mode; 155 switch (mode) { 156 case "data": 157 putValue("help", ht("/Action/ZoomToData")); 158 break; 159 case "layer": 160 putValue("help", ht("/Action/ZoomToLayer")); 161 break; 162 case "selection": 163 putValue("help", ht("/Action/ZoomToSelection")); 164 break; 165 case "conflict": 166 putValue("help", ht("/Action/ZoomToConflict")); 167 break; 168 case "problem": 169 putValue("help", ht("/Action/ZoomToProblem")); 170 break; 171 case "download": 172 putValue("help", ht("/Action/ZoomToDownload")); 173 break; 174 case "previous": 175 putValue("help", ht("/Action/ZoomToPrevious")); 176 break; 177 case "next": 178 putValue("help", ht("/Action/ZoomToNext")); 179 break; 180 default: 181 throw new IllegalArgumentException("Unknown mode: " + mode); 182 } 183 installAdapters(); 184 } 185 186 /** 187 * Performs this auto scale operation for the mode this action is in. 188 */ 189 public void autoScale() { 190 if (Main.isDisplayingMapView()) { 191 switch (mode) { 192 case "previous": 193 Main.map.mapView.zoomPrevious(); 194 break; 195 case "next": 196 Main.map.mapView.zoomNext(); 197 break; 198 default: 199 BoundingXYVisitor bbox = getBoundingBox(); 200 if (bbox != null && bbox.getBounds() != null) { 201 Main.map.mapView.zoomTo(bbox); 202 } 203 } 204 } 205 putValue("active", Boolean.TRUE); 206 } 207 208 @Override 209 public void actionPerformed(ActionEvent e) { 210 autoScale(); 211 } 212 213 /** 214 * Replies the first selected layer in the layer list dialog. null, if no 215 * such layer exists, either because the layer list dialog is not yet created 216 * or because no layer is selected. 217 * 218 * @return the first selected layer in the layer list dialog 219 */ 220 protected Layer getFirstSelectedLayer() { 221 if (Main.main.getActiveLayer() == null) { 222 return null; 223 } 224 List<Layer> layers = LayerListDialog.getInstance().getModel().getSelectedLayers(); 225 if (layers.isEmpty()) 226 return null; 227 return layers.get(0); 228 } 229 230 private BoundingXYVisitor getBoundingBox() { 231 BoundingXYVisitor v = "problem".equals(mode) ? new ValidatorBoundingXYVisitor() : new BoundingXYVisitor(); 232 233 switch (mode) { 234 case "problem": 235 TestError error = Main.map.validatorDialog.getSelectedError(); 236 if (error == null) 237 return null; 238 ((ValidatorBoundingXYVisitor) v).visit(error); 239 if (v.getBounds() == null) 240 return null; 241 v.enlargeBoundingBox(Main.pref.getDouble("validator.zoom-enlarge-bbox", 0.0002)); 242 break; 243 case "data": 244 for (Layer l : Main.map.mapView.getAllLayers()) { 245 l.visitBoundingBox(v); 246 } 247 break; 248 case "layer": 249 // try to zoom to the first selected layer 250 Layer l = getFirstSelectedLayer(); 251 if (l == null) 252 return null; 253 l.visitBoundingBox(v); 254 break; 255 case "selection": 256 case "conflict": 257 Collection<OsmPrimitive> sel = new HashSet<>(); 258 if ("selection".equals(mode)) { 259 sel = getCurrentDataSet().getSelected(); 260 } else { 261 Conflict<? extends OsmPrimitive> c = Main.map.conflictDialog.getSelectedConflict(); 262 if (c != null) { 263 sel.add(c.getMy()); 264 } else if (Main.map.conflictDialog.getConflicts() != null) { 265 sel = Main.map.conflictDialog.getConflicts().getMyConflictParties(); 266 } 267 } 268 if (sel.isEmpty()) { 269 JOptionPane.showMessageDialog( 270 Main.parent, 271 "selection".equals(mode) ? tr("Nothing selected to zoom to.") : tr("No conflicts to zoom to"), 272 tr("Information"), 273 JOptionPane.INFORMATION_MESSAGE); 274 return null; 275 } 276 for (OsmPrimitive osm : sel) { 277 osm.accept(v); 278 } 279 280 // Increase the bounding box by up to 100% to give more context. 281 v.enlargeBoundingBoxLogarithmically(100); 282 // Make the bounding box at least 100 meter wide to 283 // ensure reasonable zoom level when zooming onto single nodes. 284 v.enlargeToMinSize(Main.pref.getDouble("zoom_to_selection_min_size_in_meter", 100)); 285 break; 286 case "download": 287 288 if (lastZoomTime > 0 && System.currentTimeMillis() - lastZoomTime > Main.pref.getLong("zoom.bounds.reset.time", 10*1000)) { 289 lastZoomTime = -1; 290 } 291 final DataSet dataset = getCurrentDataSet(); 292 if (dataset != null) { 293 List<DataSource> dataSources = new ArrayList<>(dataset.getDataSources()); 294 int s = dataSources.size(); 295 if (s > 0) { 296 if (lastZoomTime == -1 || lastZoomArea == -1 || lastZoomArea > s) { 297 lastZoomArea = s-1; 298 v.visit(dataSources.get(lastZoomArea).bounds); 299 } else if (lastZoomArea > 0) { 300 lastZoomArea -= 1; 301 v.visit(dataSources.get(lastZoomArea).bounds); 302 } else { 303 lastZoomArea = -1; 304 v.visit(new Bounds(Main.main.getCurrentDataSet().getDataSourceArea().getBounds2D())); 305 } 306 lastZoomTime = System.currentTimeMillis(); 307 } else { 308 lastZoomTime = -1; 309 lastZoomArea = -1; 310 } 311 } 312 break; 313 } 314 return v; 315 } 316 317 @Override 318 protected void updateEnabledState() { 319 switch (mode) { 320 case "selection": 321 setEnabled(getCurrentDataSet() != null && !getCurrentDataSet().getSelected().isEmpty()); 322 break; 323 case "layer": 324 setEnabled(getFirstSelectedLayer() != null); 325 break; 326 case "conflict": 327 setEnabled(Main.map != null && Main.map.conflictDialog.getSelectedConflict() != null); 328 break; 329 case "download": 330 setEnabled(getCurrentDataSet() != null && !getCurrentDataSet().getDataSources().isEmpty()); 331 break; 332 case "problem": 333 setEnabled(Main.map != null && Main.map.validatorDialog.getSelectedError() != null); 334 break; 335 case "previous": 336 setEnabled(Main.isDisplayingMapView() && Main.map.mapView.hasZoomUndoEntries()); 337 break; 338 case "next": 339 setEnabled(Main.isDisplayingMapView() && Main.map.mapView.hasZoomRedoEntries()); 340 break; 341 default: 342 setEnabled(Main.isDisplayingMapView() && Main.map.mapView.hasLayers()); 343 } 344 } 345 346 @Override 347 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 348 if ("selection".equals(mode)) { 349 setEnabled(selection != null && !selection.isEmpty()); 350 } 351 } 352 353 @Override 354 protected final void installAdapters() { 355 super.installAdapters(); 356 // make this action listen to zoom and mapframe change events 357 // 358 MapView.addZoomChangeListener(new ZoomChangeAdapter()); 359 Main.addMapFrameListener(new MapFrameAdapter()); 360 initEnabledState(); 361 } 362 363 /** 364 * Adapter for zoom change events 365 */ 366 private class ZoomChangeAdapter implements MapView.ZoomChangeListener { 367 @Override 368 public void zoomChanged() { 369 updateEnabledState(); 370 } 371 } 372 373 /** 374 * Adapter for MapFrame change events 375 */ 376 private class MapFrameAdapter implements MapFrameListener { 377 private ListSelectionListener conflictSelectionListener; 378 private TreeSelectionListener validatorSelectionListener; 379 380 MapFrameAdapter() { 381 if ("conflict".equals(mode)) { 382 conflictSelectionListener = new ListSelectionListener() { 383 @Override 384 public void valueChanged(ListSelectionEvent e) { 385 updateEnabledState(); 386 } 387 }; 388 } else if ("problem".equals(mode)) { 389 validatorSelectionListener = new TreeSelectionListener() { 390 @Override 391 public void valueChanged(TreeSelectionEvent e) { 392 updateEnabledState(); 393 } 394 }; 395 } 396 } 397 398 @Override 399 public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) { 400 if (conflictSelectionListener != null) { 401 if (newFrame != null) { 402 newFrame.conflictDialog.addListSelectionListener(conflictSelectionListener); 403 } else if (oldFrame != null) { 404 oldFrame.conflictDialog.removeListSelectionListener(conflictSelectionListener); 405 } 406 } else if (validatorSelectionListener != null) { 407 if (newFrame != null) { 408 newFrame.validatorDialog.addTreeSelectionListener(validatorSelectionListener); 409 } else if (oldFrame != null) { 410 oldFrame.validatorDialog.removeTreeSelectionListener(validatorSelectionListener); 411 } 412 } 413 updateEnabledState(); 414 } 415 } 416}