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