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