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