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; 017import java.util.Objects; 018import java.util.concurrent.TimeUnit; 019 020import javax.swing.JOptionPane; 021import javax.swing.event.ListSelectionListener; 022import javax.swing.event.TreeSelectionListener; 023 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.IPrimitive; 029import org.openstreetmap.josm.data.osm.OsmData; 030import org.openstreetmap.josm.data.osm.OsmPrimitive; 031import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 032import org.openstreetmap.josm.data.validation.TestError; 033import org.openstreetmap.josm.gui.MainApplication; 034import org.openstreetmap.josm.gui.MapFrame; 035import org.openstreetmap.josm.gui.MapFrameListener; 036import org.openstreetmap.josm.gui.MapView; 037import org.openstreetmap.josm.gui.dialogs.ConflictDialog; 038import org.openstreetmap.josm.gui.dialogs.LayerListDialog; 039import org.openstreetmap.josm.gui.dialogs.ValidatorDialog.ValidatorBoundingXYVisitor; 040import org.openstreetmap.josm.gui.layer.Layer; 041import org.openstreetmap.josm.spi.preferences.Config; 042import org.openstreetmap.josm.tools.Logging; 043import org.openstreetmap.josm.tools.Shortcut; 044 045/** 046 * Toggles the autoScale feature of the mapView 047 * @author imi 048 * @since 17 049 */ 050public class AutoScaleAction extends JosmAction { 051 052 /** 053 * A list of things we can zoom to. The zoom target is given depending on the mode. 054 * @since 14221 055 */ 056 public enum AutoScaleMode { 057 /** Zoom the window so that all the data fills the window area */ 058 DATA(marktr(/* ICON(dialogs/autoscale/) */ "data")), 059 /** Zoom the window so that all the data on the currently selected layer fills the window area */ 060 LAYER(marktr(/* ICON(dialogs/autoscale/) */ "layer")), 061 /** Zoom the window so that only data which is currently selected fills the window area */ 062 SELECTION(marktr(/* ICON(dialogs/autoscale/) */ "selection")), 063 /** Zoom to the first selected conflict */ 064 CONFLICT(marktr(/* ICON(dialogs/autoscale/) */ "conflict")), 065 /** Zoom the view to last downloaded data */ 066 DOWNLOAD(marktr(/* ICON(dialogs/autoscale/) */ "download")), 067 /** Zoom the view to problem */ 068 PROBLEM(marktr(/* ICON(dialogs/autoscale/) */ "problem")), 069 /** Zoom to the previous zoomed to scale and location (zoom undo) */ 070 PREVIOUS(marktr(/* ICON(dialogs/autoscale/) */ "previous")), 071 /** Zoom to the next zoomed to scale and location (zoom redo) */ 072 NEXT(marktr(/* ICON(dialogs/autoscale/) */ "next")); 073 074 private final String label; 075 076 AutoScaleMode(String label) { 077 this.label = label; 078 } 079 080 /** 081 * Returns the English label. Used for retrieving icons. 082 * @return the English label 083 */ 084 public String getEnglishLabel() { 085 return label; 086 } 087 088 /** 089 * Returns the localized label. Used for display 090 * @return the localized label 091 */ 092 public String getLocalizedLabel() { 093 return tr(label); 094 } 095 096 /** 097 * Returns {@code AutoScaleMode} for a given English label 098 * @param englishLabel English label 099 * @return {@code AutoScaleMode} for given English label 100 * @throws IllegalArgumentException if Engligh label is unknown 101 */ 102 public static AutoScaleMode of(String englishLabel) { 103 for (AutoScaleMode v : values()) { 104 if (Objects.equals(v.label, englishLabel)) { 105 return v; 106 } 107 } 108 throw new IllegalArgumentException(englishLabel); 109 } 110 } 111 112 /** 113 * A list of things we can zoom to. The zoom target is given depending on the mode. 114 * @deprecated Use {@link AutoScaleMode} enum instead 115 */ 116 @Deprecated 117 public static final Collection<String> MODES = Collections.unmodifiableList(Arrays.asList( 118 marktr(/* ICON(dialogs/autoscale/) */ "data"), 119 marktr(/* ICON(dialogs/autoscale/) */ "layer"), 120 marktr(/* ICON(dialogs/autoscale/) */ "selection"), 121 marktr(/* ICON(dialogs/autoscale/) */ "conflict"), 122 marktr(/* ICON(dialogs/autoscale/) */ "download"), 123 marktr(/* ICON(dialogs/autoscale/) */ "problem"), 124 marktr(/* ICON(dialogs/autoscale/) */ "previous"), 125 marktr(/* ICON(dialogs/autoscale/) */ "next"))); 126 127 /** 128 * One of {@link AutoScaleMode}. Defines what we are zooming to. 129 */ 130 private final AutoScaleMode mode; 131 132 /** Time of last zoom to bounds action */ 133 protected long lastZoomTime = -1; 134 /** Last zommed bounds */ 135 protected int lastZoomArea = -1; 136 137 /** 138 * Zooms the current map view to the currently selected primitives. 139 * Does nothing if there either isn't a current map view or if there isn't a current data layer. 140 * 141 */ 142 public static void zoomToSelection() { 143 OsmData<?, ?, ?, ?> dataSet = MainApplication.getLayerManager().getActiveData(); 144 if (dataSet == null) { 145 return; 146 } 147 Collection<? extends IPrimitive> sel = dataSet.getSelected(); 148 if (sel.isEmpty()) { 149 JOptionPane.showMessageDialog( 150 MainApplication.getMainFrame(), 151 tr("Nothing selected to zoom to."), 152 tr("Information"), 153 JOptionPane.INFORMATION_MESSAGE); 154 return; 155 } 156 zoomTo(sel); 157 } 158 159 /** 160 * Zooms the view to display the given set of primitives. 161 * @param sel The primitives to zoom to, e.g. the current selection. 162 */ 163 public static void zoomTo(Collection<? extends IPrimitive> sel) { 164 BoundingXYVisitor bboxCalculator = new BoundingXYVisitor(); 165 bboxCalculator.computeBoundingBox(sel); 166 if (bboxCalculator.getBounds() != null) { 167 MainApplication.getMap().mapView.zoomTo(bboxCalculator); 168 } 169 } 170 171 /** 172 * Performs the auto scale operation of the given mode without the need to create a new action. 173 * @param mode One of {@link #MODES}. 174 * @since 14221 175 */ 176 public static void autoScale(AutoScaleMode mode) { 177 new AutoScaleAction(mode, false).autoScale(); 178 } 179 180 /** 181 * Performs the auto scale operation of the given mode without the need to create a new action. 182 * @param mode One of {@link #MODES}. 183 * @deprecated Use {@link #autoScale(AutoScaleMode)} instead 184 */ 185 @Deprecated 186 public static void autoScale(String mode) { 187 autoScale(AutoScaleMode.of(mode)); 188 } 189 190 private static int getModeShortcut(String mode) { 191 int shortcut = -1; 192 193 // TODO: convert this to switch/case and make sure the parsing still works 194 // CHECKSTYLE.OFF: LeftCurly 195 // CHECKSTYLE.OFF: RightCurly 196 /* leave as single line for shortcut overview parsing! */ 197 if (mode.equals("data")) { shortcut = KeyEvent.VK_1; } 198 else if (mode.equals("layer")) { shortcut = KeyEvent.VK_2; } 199 else if (mode.equals("selection")) { shortcut = KeyEvent.VK_3; } 200 else if (mode.equals("conflict")) { shortcut = KeyEvent.VK_4; } 201 else if (mode.equals("download")) { shortcut = KeyEvent.VK_5; } 202 else if (mode.equals("problem")) { shortcut = KeyEvent.VK_6; } 203 else if (mode.equals("previous")) { shortcut = KeyEvent.VK_8; } 204 else if (mode.equals("next")) { shortcut = KeyEvent.VK_9; } 205 // CHECKSTYLE.ON: LeftCurly 206 // CHECKSTYLE.ON: RightCurly 207 208 return shortcut; 209 } 210 211 /** 212 * Constructs a new {@code AutoScaleAction}. 213 * @param mode The autoscale mode (one of {@link AutoScaleMode}) 214 * @param marker Must be set to false. Used only to differentiate from default constructor 215 */ 216 private AutoScaleAction(AutoScaleMode mode, boolean marker) { 217 super(marker); 218 this.mode = mode; 219 } 220 221 /** 222 * Constructs a new {@code AutoScaleAction}. 223 * @param mode The autoscale mode (one of {@link AutoScaleAction#MODES}) 224 * @deprecated Use {@link #AutoScaleAction(AutoScaleMode)} instead 225 */ 226 @Deprecated 227 public AutoScaleAction(final String mode) { 228 this(AutoScaleMode.of(mode)); 229 } 230 231 /** 232 * Constructs a new {@code AutoScaleAction}. 233 * @param mode The autoscale mode (one of {@link AutoScaleAction#MODES}) 234 * @since 14221 235 */ 236 public AutoScaleAction(final AutoScaleMode mode) { 237 super(tr("Zoom to {0}", mode.getLocalizedLabel()), "dialogs/autoscale/" + mode.getEnglishLabel(), 238 tr("Zoom the view to {0}.", mode.getLocalizedLabel()), 239 Shortcut.registerShortcut("view:zoom" + mode.getEnglishLabel(), 240 tr("View: {0}", tr("Zoom to {0}", mode.getLocalizedLabel())), 241 getModeShortcut(mode.getEnglishLabel()), Shortcut.DIRECT), true, null, false); 242 String label = mode.getEnglishLabel(); 243 String modeHelp = Character.toUpperCase(label.charAt(0)) + label.substring(1); 244 setHelpId("Action/AutoScale/" + modeHelp); 245 this.mode = mode; 246 switch (mode) { 247 case DATA: 248 setHelpId(ht("/Action/ZoomToData")); 249 break; 250 case LAYER: 251 setHelpId(ht("/Action/ZoomToLayer")); 252 break; 253 case SELECTION: 254 setHelpId(ht("/Action/ZoomToSelection")); 255 break; 256 case CONFLICT: 257 setHelpId(ht("/Action/ZoomToConflict")); 258 break; 259 case PROBLEM: 260 setHelpId(ht("/Action/ZoomToProblem")); 261 break; 262 case DOWNLOAD: 263 setHelpId(ht("/Action/ZoomToDownload")); 264 break; 265 case PREVIOUS: 266 setHelpId(ht("/Action/ZoomToPrevious")); 267 break; 268 case NEXT: 269 setHelpId(ht("/Action/ZoomToNext")); 270 break; 271 default: 272 throw new IllegalArgumentException("Unknown mode: " + mode); 273 } 274 installAdapters(); 275 } 276 277 /** 278 * Performs this auto scale operation for the mode this action is in. 279 */ 280 public void autoScale() { 281 if (MainApplication.isDisplayingMapView()) { 282 MapView mapView = MainApplication.getMap().mapView; 283 switch (mode) { 284 case PREVIOUS: 285 mapView.zoomPrevious(); 286 break; 287 case NEXT: 288 mapView.zoomNext(); 289 break; 290 case PROBLEM: 291 modeProblem(new ValidatorBoundingXYVisitor()); 292 break; 293 case DATA: 294 modeData(new BoundingXYVisitor()); 295 break; 296 case LAYER: 297 modeLayer(new BoundingXYVisitor()); 298 break; 299 case SELECTION: 300 case CONFLICT: 301 modeSelectionOrConflict(new BoundingXYVisitor()); 302 break; 303 case DOWNLOAD: 304 modeDownload(new BoundingXYVisitor()); 305 break; 306 } 307 putValue("active", Boolean.TRUE); 308 } 309 } 310 311 @Override 312 public void actionPerformed(ActionEvent e) { 313 autoScale(); 314 } 315 316 /** 317 * Replies the first selected layer in the layer list dialog. null, if no 318 * such layer exists, either because the layer list dialog is not yet created 319 * or because no layer is selected. 320 * 321 * @return the first selected layer in the layer list dialog 322 */ 323 protected Layer getFirstSelectedLayer() { 324 if (getLayerManager().getActiveLayer() == null) { 325 return null; 326 } 327 try { 328 List<Layer> layers = LayerListDialog.getInstance().getModel().getSelectedLayers(); 329 if (!layers.isEmpty()) 330 return layers.get(0); 331 } catch (IllegalStateException e) { 332 Logging.error(e); 333 } 334 return null; 335 } 336 337 private static void modeProblem(ValidatorBoundingXYVisitor v) { 338 TestError error = MainApplication.getMap().validatorDialog.getSelectedError(); 339 if (error == null) 340 return; 341 v.visit(error); 342 if (v.getBounds() == null) 343 return; 344 MainApplication.getMap().mapView.zoomTo(v); 345 } 346 347 private static void modeData(BoundingXYVisitor v) { 348 for (Layer l : MainApplication.getLayerManager().getLayers()) { 349 l.visitBoundingBox(v); 350 } 351 MainApplication.getMap().mapView.zoomTo(v); 352 } 353 354 private void modeLayer(BoundingXYVisitor v) { 355 // try to zoom to the first selected layer 356 Layer l = getFirstSelectedLayer(); 357 if (l == null) 358 return; 359 l.visitBoundingBox(v); 360 MainApplication.getMap().mapView.zoomTo(v); 361 } 362 363 private void modeSelectionOrConflict(BoundingXYVisitor v) { 364 Collection<IPrimitive> sel = new HashSet<>(); 365 if (AutoScaleMode.SELECTION == mode) { 366 OsmData<?, ?, ?, ?> dataSet = getLayerManager().getActiveData(); 367 if (dataSet != null) { 368 sel.addAll(dataSet.getSelected()); 369 } 370 } else { 371 ConflictDialog conflictDialog = MainApplication.getMap().conflictDialog; 372 Conflict<? extends IPrimitive> c = conflictDialog.getSelectedConflict(); 373 if (c != null) { 374 sel.add(c.getMy()); 375 } else if (conflictDialog.getConflicts() != null) { 376 sel.addAll(conflictDialog.getConflicts().getMyConflictParties()); 377 } 378 } 379 if (sel.isEmpty()) { 380 JOptionPane.showMessageDialog( 381 MainApplication.getMainFrame(), 382 AutoScaleMode.SELECTION == mode ? tr("Nothing selected to zoom to.") : tr("No conflicts to zoom to"), 383 tr("Information"), 384 JOptionPane.INFORMATION_MESSAGE); 385 return; 386 } 387 for (IPrimitive osm : sel) { 388 osm.accept(v); 389 } 390 if (v.getBounds() == null) { 391 return; 392 } 393 394 MainApplication.getMap().mapView.zoomTo(v); 395 } 396 397 private void modeDownload(BoundingXYVisitor v) { 398 if (lastZoomTime > 0 && 399 System.currentTimeMillis() - lastZoomTime > Config.getPref().getLong("zoom.bounds.reset.time", TimeUnit.SECONDS.toMillis(10))) { 400 lastZoomTime = -1; 401 } 402 final DataSet dataset = getLayerManager().getActiveDataSet(); 403 if (dataset != null) { 404 List<DataSource> dataSources = new ArrayList<>(dataset.getDataSources()); 405 int s = dataSources.size(); 406 if (s > 0) { 407 if (lastZoomTime == -1 || lastZoomArea == -1 || lastZoomArea > s) { 408 lastZoomArea = s-1; 409 v.visit(dataSources.get(lastZoomArea).bounds); 410 } else if (lastZoomArea > 0) { 411 lastZoomArea -= 1; 412 v.visit(dataSources.get(lastZoomArea).bounds); 413 } else { 414 lastZoomArea = -1; 415 Area sourceArea = getLayerManager().getActiveDataSet().getDataSourceArea(); 416 if (sourceArea != null) { 417 v.visit(new Bounds(sourceArea.getBounds2D())); 418 } 419 } 420 lastZoomTime = System.currentTimeMillis(); 421 } else { 422 lastZoomTime = -1; 423 lastZoomArea = -1; 424 } 425 } 426 MainApplication.getMap().mapView.zoomTo(v); 427 } 428 429 @Override 430 protected void updateEnabledState() { 431 OsmData<?, ?, ?, ?> ds = getLayerManager().getActiveData(); 432 MapFrame map = MainApplication.getMap(); 433 switch (mode) { 434 case SELECTION: 435 setEnabled(ds != null && !ds.selectionEmpty()); 436 break; 437 case LAYER: 438 setEnabled(getFirstSelectedLayer() != null); 439 break; 440 case CONFLICT: 441 setEnabled(map != null && map.conflictDialog.getSelectedConflict() != null); 442 break; 443 case DOWNLOAD: 444 setEnabled(ds != null && !ds.getDataSources().isEmpty()); 445 break; 446 case PROBLEM: 447 setEnabled(map != null && map.validatorDialog.getSelectedError() != null); 448 break; 449 case PREVIOUS: 450 setEnabled(MainApplication.isDisplayingMapView() && map.mapView.hasZoomUndoEntries()); 451 break; 452 case NEXT: 453 setEnabled(MainApplication.isDisplayingMapView() && map.mapView.hasZoomRedoEntries()); 454 break; 455 default: 456 setEnabled(!getLayerManager().getLayers().isEmpty()); 457 } 458 } 459 460 @Override 461 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 462 if (AutoScaleMode.SELECTION == mode) { 463 setEnabled(selection != null && !selection.isEmpty()); 464 } 465 } 466 467 @Override 468 protected final void installAdapters() { 469 super.installAdapters(); 470 // make this action listen to zoom and mapframe change events 471 // 472 MapView.addZoomChangeListener(new ZoomChangeAdapter()); 473 MainApplication.addMapFrameListener(new MapFrameAdapter()); 474 initEnabledState(); 475 } 476 477 /** 478 * Adapter for zoom change events 479 */ 480 private class ZoomChangeAdapter implements MapView.ZoomChangeListener { 481 @Override 482 public void zoomChanged() { 483 updateEnabledState(); 484 } 485 } 486 487 /** 488 * Adapter for MapFrame change events 489 */ 490 private class MapFrameAdapter implements MapFrameListener { 491 private ListSelectionListener conflictSelectionListener; 492 private TreeSelectionListener validatorSelectionListener; 493 494 MapFrameAdapter() { 495 if (AutoScaleMode.CONFLICT == mode) { 496 conflictSelectionListener = e -> updateEnabledState(); 497 } else if (AutoScaleMode.PROBLEM == mode) { 498 validatorSelectionListener = e -> updateEnabledState(); 499 } 500 } 501 502 @Override 503 public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) { 504 if (conflictSelectionListener != null) { 505 if (newFrame != null) { 506 newFrame.conflictDialog.addListSelectionListener(conflictSelectionListener); 507 } else if (oldFrame != null) { 508 oldFrame.conflictDialog.removeListSelectionListener(conflictSelectionListener); 509 } 510 } else if (validatorSelectionListener != null) { 511 if (newFrame != null) { 512 newFrame.validatorDialog.addTreeSelectionListener(validatorSelectionListener); 513 } else if (oldFrame != null) { 514 oldFrame.validatorDialog.removeTreeSelectionListener(validatorSelectionListener); 515 } 516 } 517 updateEnabledState(); 518 } 519 } 520}