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

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

fix remaining checkstyle issues

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