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