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