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

Last change on this file since 10716 was 10716, checked in by simon04, 8 years ago

see #11390, see #12890 - Deprecate predicates in OsmPrimitive class

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