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

Last change on this file since 7217 was 7217, checked in by akks, 10 years ago

see #10104: refactor key press/release detection introducing Main.map.keyDetector

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