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