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

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

cleanup/refactor of NavigatableComponent

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