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