source: josm/trunk/src/org/openstreetmap/josm/actions/mapmode/ParallelWayAction.java@ 5982

Last change on this file since 5982 was 5982, checked in by Don-vip, 11 years ago

ParallelWayAction: handle nautical mile SoM + listen to preferences changes

  • Property svn:eol-style set to native
File size: 21.1 KB
Line 
1// License: GPL. See LICENSE file for details.
2package org.openstreetmap.josm.actions.mapmode;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.awt.AWTEvent;
8import java.awt.BasicStroke;
9import java.awt.Color;
10import java.awt.Cursor;
11import java.awt.Graphics2D;
12import java.awt.Point;
13import java.awt.Stroke;
14import java.awt.Toolkit;
15import java.awt.event.AWTEventListener;
16import java.awt.event.InputEvent;
17import java.awt.event.KeyEvent;
18import java.awt.event.MouseEvent;
19import java.util.Collection;
20import java.util.LinkedHashSet;
21
22import javax.swing.JOptionPane;
23
24import org.openstreetmap.josm.Main;
25import org.openstreetmap.josm.data.Bounds;
26import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
27import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
28import org.openstreetmap.josm.data.coor.EastNorth;
29import org.openstreetmap.josm.data.osm.Node;
30import org.openstreetmap.josm.data.osm.OsmPrimitive;
31import org.openstreetmap.josm.data.osm.Way;
32import org.openstreetmap.josm.data.osm.WaySegment;
33import org.openstreetmap.josm.gui.MapFrame;
34import org.openstreetmap.josm.gui.MapView;
35import org.openstreetmap.josm.gui.NavigatableComponent;
36import org.openstreetmap.josm.gui.NavigatableComponent.SystemOfMeasurement;
37import org.openstreetmap.josm.gui.layer.Layer;
38import org.openstreetmap.josm.gui.layer.MapViewPaintable;
39import org.openstreetmap.josm.gui.layer.OsmDataLayer;
40import org.openstreetmap.josm.tools.Geometry;
41import org.openstreetmap.josm.tools.ImageProvider;
42import org.openstreetmap.josm.tools.Shortcut;
43
44//// TODO: (list below)
45/* == Functionality ==
46 *
47 * 1. Use selected nodes as split points for the selected ways.
48 *
49 * The ways containing the selected nodes will be split and only the "inner"
50 * parts will be copied
51 *
52 * 2. Enter exact offset
53 *
54 * 3. Improve snapping
55 *
56 * 4. Visual cues could be better
57 *
58 * 5. Cursors (Half-done)
59 *
60 * 6. (long term) Parallelize and adjust offsets of existing ways
61 *
62 * == Code quality ==
63 *
64 * a) The mode, flags, and modifiers might be updated more than necessary.
65 *
66 * Not a performance problem, but better if they where more centralized
67 *
68 * b) Extract generic MapMode services into a super class and/or utility class
69 *
70 * c) Maybe better to simply draw our own source way highlighting?
71 *
72 * Current code doesn't not take into account that ways might been highlighted
73 * by other than us. Don't think that situation should ever happen though.
74 */
75
76/**
77 * MapMode for making parallel ways.
78 *
79 * All calculations are done in projected coordinates.
80 *
81 * @author Ole Jørgen Brønner (olejorgenb)
82 */
83public class ParallelWayAction extends MapMode implements AWTEventListener, MapViewPaintable, PreferenceChangedListener {
84
85 private enum Mode {
86 dragging, normal
87 }
88
89 //// Preferences and flags
90 // See updateModeLocalPreferences for defaults
91 private Mode mode;
92 private boolean copyTags;
93 private boolean copyTagsDefault;
94
95 private boolean snap;
96 private boolean snapDefault;
97
98 private double snapThreshold;
99 private double snapDistanceMetric;
100 private double snapDistanceImperial;
101 private double snapDistanceChinese;
102 private double snapDistanceNautical;
103
104 private ModifiersSpec snapModifierCombo;
105 private ModifiersSpec copyTagsModifierCombo;
106 private ModifiersSpec addToSelectionModifierCombo;
107 private ModifiersSpec toggleSelectedModifierCombo;
108 private ModifiersSpec setSelectedModifierCombo;
109
110 private int initialMoveDelay;
111
112 private final MapView mv;
113
114 // Mouse tracking state
115 private Point mousePressedPos;
116 private boolean mouseIsDown;
117 private long mousePressedTime;
118 private boolean mouseHasBeenDragged;
119
120 private WaySegment referenceSegment;
121 private ParallelWays pWays;
122 private LinkedHashSet<Way> sourceWays;
123 private EastNorth helperLineStart;
124 private EastNorth helperLineEnd;
125
126 public ParallelWayAction(MapFrame mapFrame) {
127 super(tr("Parallel"), "parallel", tr("Make parallel copies of ways"),
128 Shortcut.registerShortcut("mapmode:parallel", tr("Mode: {0}",
129 tr("Parallel")), KeyEvent.VK_P, Shortcut.SHIFT),
130 mapFrame, ImageProvider.getCursor("normal", "parallel"));
131 putValue("help", ht("/Action/Parallel"));
132 mv = mapFrame.mapView;
133 updateModeLocalPreferences();
134 Main.pref.addPreferenceChangeListener(this);
135 }
136
137 @Override
138 public void enterMode() {
139 // super.enterMode() updates the status line and cursor so we need our state to be set correctly
140 setMode(Mode.normal);
141 pWays = null;
142 updateAllPreferences(); // All default values should've been set now
143
144 super.enterMode();
145
146 mv.addMouseListener(this);
147 mv.addMouseMotionListener(this);
148 mv.addTemporaryLayer(this);
149
150 //// Needed to update the mouse cursor if modifiers are changed when the mouse is motionless
151 try {
152 Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK);
153 } catch (SecurityException ex) {
154 }
155 sourceWays = new LinkedHashSet<Way>(getCurrentDataSet().getSelectedWays());
156 for (Way w : sourceWays) {
157 w.setHighlighted(true);
158 }
159 mv.repaint();
160 }
161
162 @Override
163 public void exitMode() {
164 super.exitMode();
165 mv.removeMouseListener(this);
166 mv.removeMouseMotionListener(this);
167 mv.removeTemporaryLayer(this);
168 Main.map.statusLine.setDist(-1);
169 Main.map.statusLine.repaint();
170 try {
171 Toolkit.getDefaultToolkit().removeAWTEventListener(this);
172 } catch (SecurityException ex) {
173 }
174 removeWayHighlighting(sourceWays);
175 pWays = null;
176 sourceWays = null;
177 referenceSegment = null;
178 mv.repaint();
179 }
180
181 @Override
182 public String getModeHelpText() {
183 // TODO: add more detailed feedback based on modifier state.
184 // TODO: dynamic messages based on preferences. (Could be problematic translation wise)
185 switch (mode) {
186 case normal:
187 return tr("Select ways as in Select mode. Drag selected ways or a single way to create a parallel copy (Alt toggles tag preservation)");
188 case dragging:
189 return tr("Hold Ctrl to toggle snapping");
190 }
191 return ""; // impossible ..
192 }
193
194 // Separated due to "race condition" between default values
195 private void updateAllPreferences() {
196 updateModeLocalPreferences();
197 // @formatter:off
198 // @formatter:on
199 }
200
201 private void updateModeLocalPreferences() {
202 // @formatter:off
203 snapThreshold = Main.pref.getDouble (prefKey("snap-threshold-percent"), 0.70);
204 snapDefault = Main.pref.getBoolean(prefKey("snap-default"), true);
205 copyTagsDefault = Main.pref.getBoolean(prefKey("copy-tags-default"), true);
206 initialMoveDelay = Main.pref.getInteger(prefKey("initial-move-delay"), 200);
207 snapDistanceMetric = Main.pref.getDouble(prefKey("snap-distance-metric"), 0.5);
208 snapDistanceImperial = Main.pref.getDouble(prefKey("snap-distance-imperial"), 1);
209 snapDistanceChinese = Main.pref.getDouble(prefKey("snap-distance-chinese"), 1);
210 snapDistanceNautical = Main.pref.getDouble(prefKey("snap-distance-nautical"), 0.1);
211
212 snapModifierCombo = new ModifiersSpec(getStringPref("snap-modifier-combo", "?sC"));
213 copyTagsModifierCombo = new ModifiersSpec(getStringPref("copy-tags-modifier-combo", "As?"));
214 addToSelectionModifierCombo = new ModifiersSpec(getStringPref("add-to-selection-modifier-combo", "aSc"));
215 toggleSelectedModifierCombo = new ModifiersSpec(getStringPref("toggle-selection-modifier-combo", "asC"));
216 setSelectedModifierCombo = new ModifiersSpec(getStringPref("set-selection-modifier-combo", "asc"));
217 // @formatter:on
218 }
219
220 @Override
221 public boolean layerIsSupported(Layer layer) {
222 return layer instanceof OsmDataLayer;
223 }
224
225 @Override
226 public void eventDispatched(AWTEvent e) {
227 if (Main.map == null || mv == null || !mv.isActiveLayerDrawable())
228 return;
229
230 // Should only get InputEvents due to the mask in enterMode
231 if (updateModifiersState((InputEvent) e)) {
232 updateStatusLine();
233 updateCursor();
234 }
235 }
236
237 private boolean updateModifiersState(InputEvent e) {
238 boolean oldAlt = alt, oldShift = shift, oldCtrl = ctrl;
239 updateKeyModifiers(e);
240 boolean changed = (oldAlt != alt || oldShift != shift || oldCtrl != ctrl);
241 return changed;
242 }
243
244 private void updateCursor() {
245 Cursor newCursor = null;
246 switch (mode) {
247 case normal:
248 if (matchesCurrentModifiers(setSelectedModifierCombo)) {
249 newCursor = ImageProvider.getCursor("normal", "parallel");
250 } else if (matchesCurrentModifiers(addToSelectionModifierCombo)) {
251 newCursor = ImageProvider.getCursor("normal", "parallel"); // FIXME
252 } else if (matchesCurrentModifiers(toggleSelectedModifierCombo)) {
253 newCursor = ImageProvider.getCursor("normal", "parallel"); // FIXME
254 } else {
255 // TODO: set to a cursor indicating an error
256 }
257 break;
258 case dragging:
259 if (snap) {
260 // TODO: snapping cursor?
261 newCursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
262 } else {
263 newCursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
264 }
265 }
266 if (newCursor != null) {
267 mv.setNewCursor(newCursor, this);
268 }
269 }
270
271 private void setMode(Mode mode) {
272 this.mode = mode;
273 updateCursor();
274 updateStatusLine();
275 }
276
277 private boolean isValidModifierCombination() {
278 // TODO: implement to give feedback on invalid modifier combination
279 return true;
280 }
281
282 private boolean sanityCheck() {
283 // @formatter:off
284 boolean areWeSane =
285 mv.isActiveLayerVisible() &&
286 mv.isActiveLayerDrawable() &&
287 ((Boolean) this.getValue("active"));
288 // @formatter:on
289 assert (areWeSane); // mad == bad
290 return areWeSane;
291 }
292
293 @Override
294 public void mousePressed(MouseEvent e) {
295 requestFocusInMapView();
296 updateModifiersState(e);
297 // Other buttons are off limit, but we still get events.
298 if (e.getButton() != MouseEvent.BUTTON1)
299 return;
300
301 if(sanityCheck() == false)
302 return;
303
304 updateFlagsOnlyChangeableOnPress();
305 updateFlagsChangeableAlways();
306
307 // Since the created way is left selected, we need to unselect again here
308 if (pWays != null && pWays.ways != null) {
309 getCurrentDataSet().clearSelection(pWays.ways);
310 pWays = null;
311 }
312
313 mouseIsDown = true;
314 mousePressedPos = e.getPoint();
315 mousePressedTime = System.currentTimeMillis();
316
317 }
318
319 @Override
320 public void mouseReleased(MouseEvent e) {
321 updateModifiersState(e);
322 // Other buttons are off limit, but we still get events.
323 if (e.getButton() != MouseEvent.BUTTON1)
324 return;
325
326 if (!mouseHasBeenDragged) {
327 // use point from press or click event? (or are these always the same)
328 Way nearestWay = mv.getNearestWay(e.getPoint(), OsmPrimitive.isSelectablePredicate);
329 if (nearestWay == null) {
330 if (matchesCurrentModifiers(setSelectedModifierCombo)) {
331 clearSourceWays();
332 }
333 resetMouseTrackingState();
334 return;
335 }
336 boolean isSelected = nearestWay.isSelected();
337 if (matchesCurrentModifiers(addToSelectionModifierCombo)) {
338 if (!isSelected) {
339 addSourceWay(nearestWay);
340 }
341 } else if (matchesCurrentModifiers(toggleSelectedModifierCombo)) {
342 if (isSelected) {
343 removeSourceWay(nearestWay);
344 } else {
345 addSourceWay(nearestWay);
346 }
347 } else if (matchesCurrentModifiers(setSelectedModifierCombo)) {
348 clearSourceWays();
349 addSourceWay(nearestWay);
350 } // else -> invalid modifier combination
351 } else if (mode == Mode.dragging) {
352 clearSourceWays();
353 }
354
355 setMode(Mode.normal);
356 resetMouseTrackingState();
357 mv.repaint();
358 }
359
360 private void removeWayHighlighting(Collection<Way> ways) {
361 if (ways == null)
362 return;
363 for (Way w : ways) {
364 w.setHighlighted(false);
365 }
366 }
367
368 @Override
369 public void mouseDragged(MouseEvent e) {
370 // WTF.. the event passed here doesn't have button info?
371 // Since we get this event from other buttons too, we must check that
372 // _BUTTON1_ is down.
373 if (!mouseIsDown)
374 return;
375
376 boolean modifiersChanged = updateModifiersState(e);
377 updateFlagsChangeableAlways();
378
379 if (modifiersChanged) {
380 // Since this could be remotely slow, do it conditionally
381 updateStatusLine();
382 updateCursor();
383 }
384
385 if ((System.currentTimeMillis() - mousePressedTime) < initialMoveDelay)
386 return;
387 // Assuming this event only is emitted when the mouse has moved
388 // Setting this after the check above means we tolerate clicks with some movement
389 mouseHasBeenDragged = true;
390
391 Point p = e.getPoint();
392 if (mode == Mode.normal) {
393 // Should we ensure that the copyTags modifiers are still valid?
394
395 // Important to use mouse position from the press, since the drag
396 // event can come quite late
397 if (!isModifiersValidForDragMode())
398 return;
399 if (!initParallelWays(mousePressedPos, copyTags))
400 return;
401 setMode(Mode.dragging);
402 }
403
404 // Calculate distance to the reference line
405 EastNorth enp = mv.getEastNorth((int) p.getX(), (int) p.getY());
406 EastNorth nearestPointOnRefLine = Geometry.closestPointToLine(referenceSegment.getFirstNode().getEastNorth(),
407 referenceSegment.getSecondNode().getEastNorth(), enp);
408
409 // Note: d is the distance in _projected units_
410 double d = enp.distance(nearestPointOnRefLine);
411 double realD = mv.getProjection().eastNorth2latlon(enp).greatCircleDistance(mv.getProjection().eastNorth2latlon(nearestPointOnRefLine));
412 double snappedRealD = realD;
413
414 // TODO: abuse of isToTheRightSideOfLine function.
415 boolean toTheRight = Geometry.isToTheRightSideOfLine(referenceSegment.getFirstNode(),
416 referenceSegment.getFirstNode(), referenceSegment.getSecondNode(), new Node(enp));
417
418 if (snap) {
419 // TODO: Very simple snapping
420 // - Snap steps relative to the distance?
421 double snapDistance;
422 SystemOfMeasurement som = NavigatableComponent.getSystemOfMeasurement();
423 if (som.equals(NavigatableComponent.CHINESE_SOM)) {
424 snapDistance = snapDistanceChinese * NavigatableComponent.CHINESE_SOM.aValue;
425 } else if (som.equals(NavigatableComponent.IMPERIAL_SOM)) {
426 snapDistance = snapDistanceImperial * NavigatableComponent.IMPERIAL_SOM.aValue;
427 } else if (som.equals(NavigatableComponent.NAUTICAL_MILE_SOM)) {
428 snapDistance = snapDistanceNautical * NavigatableComponent.NAUTICAL_MILE_SOM.aValue;
429 } else {
430 snapDistance = snapDistanceMetric; // Metric system by default
431 }
432 double closestWholeUnit;
433 double modulo = realD % snapDistance;
434 if (modulo < snapDistance/2.0) {
435 closestWholeUnit = realD - modulo;
436 } else {
437 closestWholeUnit = realD + (snapDistance-modulo);
438 }
439 if (Math.abs(closestWholeUnit - realD) < (snapThreshold * snapDistance)) {
440 snappedRealD = closestWholeUnit;
441 } else {
442 snappedRealD = closestWholeUnit + Math.signum(realD - closestWholeUnit) * snapDistance;
443 }
444 }
445 d = snappedRealD * (d/realD); // convert back to projected distance. (probably ok on small scales)
446 helperLineStart = nearestPointOnRefLine;
447 helperLineEnd = enp;
448 if (toTheRight) {
449 d = -d;
450 }
451 pWays.changeOffset(d);
452
453 Main.map.statusLine.setDist(Math.abs(snappedRealD));
454 Main.map.statusLine.repaint();
455 mv.repaint();
456 }
457
458 private boolean matchesCurrentModifiers(ModifiersSpec spec) {
459 return spec.matchWithKnown(alt, shift, ctrl);
460 }
461
462 @Override
463 public void paint(Graphics2D g, MapView mv, Bounds bbox) {
464 if (mode == Mode.dragging) {
465 // sanity checks
466 if (mv == null)
467 return;
468
469 // FIXME: should clip the line (gets insanely slow when zoomed in on a very long line
470 Stroke refLineStroke = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 10.0f, new float[] {
471 2f, 2f }, 0f);
472 g.setStroke(refLineStroke);
473 g.setColor(Color.RED);
474 Point p1 = mv.getPoint(referenceSegment.getFirstNode().getEastNorth());
475 Point p2 = mv.getPoint(referenceSegment.getSecondNode().getEastNorth());
476 g.drawLine(p1.x, p1.y, p2.x, p2.y);
477
478 Stroke helpLineStroke = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL);
479 g.setStroke(helpLineStroke);
480 g.setColor(Color.RED);
481 p1 = mv.getPoint(helperLineStart);
482 p2 = mv.getPoint(helperLineEnd);
483 g.drawLine(p1.x, p1.y, p2.x, p2.y);
484 }
485 }
486
487 private boolean isModifiersValidForDragMode() {
488 return (!alt && !shift && !ctrl) || matchesCurrentModifiers(snapModifierCombo)
489 || matchesCurrentModifiers(copyTagsModifierCombo);
490 }
491
492 private void updateFlagsOnlyChangeableOnPress() {
493 copyTags = copyTagsDefault != matchesCurrentModifiers(copyTagsModifierCombo);
494 }
495
496 private void updateFlagsChangeableAlways() {
497 snap = snapDefault != matchesCurrentModifiers(snapModifierCombo);
498 }
499
500 //// We keep the source ways and the selection in sync so the user can see the source way's tags
501 private void addSourceWay(Way w) {
502 assert (sourceWays != null);
503 getCurrentDataSet().addSelected(w);
504 w.setHighlighted(true);
505 sourceWays.add(w);
506 }
507
508 private void removeSourceWay(Way w) {
509 assert (sourceWays != null);
510 getCurrentDataSet().clearSelection(w);
511 w.setHighlighted(false);
512 sourceWays.remove(w);
513 }
514
515 private void clearSourceWays() {
516 assert (sourceWays != null);
517 if (sourceWays == null)
518 return;
519 getCurrentDataSet().clearSelection(sourceWays);
520 for (Way w : sourceWays) {
521 w.setHighlighted(false);
522 }
523 sourceWays.clear();
524 }
525
526 private void resetMouseTrackingState() {
527 mouseIsDown = false;
528 mousePressedPos = null;
529 mouseHasBeenDragged = false;
530 }
531
532 // TODO: rename
533 private boolean initParallelWays(Point p, boolean copyTags) {
534 referenceSegment = mv.getNearestWaySegment(p, Way.isUsablePredicate, true);
535 if (referenceSegment == null)
536 return false;
537
538 if (!sourceWays.contains(referenceSegment.way)) {
539 clearSourceWays();
540 addSourceWay(referenceSegment.way);
541 }
542
543 try {
544 int referenceWayIndex = -1;
545 int i = 0;
546 for (Way w : sourceWays) {
547 if (w == referenceSegment.way) {
548 referenceWayIndex = i;
549 break;
550 }
551 i++;
552 }
553 pWays = new ParallelWays(sourceWays, copyTags, referenceWayIndex);
554 pWays.commit();
555 getCurrentDataSet().setSelected(pWays.ways);
556 return true;
557 } catch (IllegalArgumentException e) {
558 // TODO: Not ideal feedback. Maybe changing the cursor could be a good mechanism?
559 JOptionPane.showMessageDialog(
560 Main.parent,
561 tr("ParallelWayAction\n" +
562 "The ways selected must form a simple branchless path"),
563 tr("Make parallel way error"),
564 JOptionPane.INFORMATION_MESSAGE);
565 // The error dialog prevents us from getting the mouseReleased event
566 resetMouseTrackingState();
567 pWays = null;
568 return false;
569 }
570 }
571
572 private String prefKey(String subKey) {
573 return "edit.make-parallel-way-action." + subKey;
574 }
575
576 private String getStringPref(String subKey, String def) {
577 return Main.pref.get(prefKey(subKey), def);
578 }
579
580 private String getStringPref(String subKey) {
581 return getStringPref(subKey, null);
582 }
583
584 @Override
585 public void preferenceChanged(PreferenceChangeEvent e) {
586 if (e.getKey().startsWith(prefKey(""))) {
587 updateAllPreferences();
588 }
589 }
590}
Note: See TracBrowser for help on using the repository browser.