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

Last change on this file since 4251 was 4251, checked in by bastiK, 13 years ago

applied #6598 - Parallelway mode reports wrong offset distance in statusline (patch by olejorgenb)

  • Property svn:eol-style set to native
File size: 19.6 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.ActionEvent;
17import java.awt.event.InputEvent;
18import java.awt.event.KeyEvent;
19import java.awt.event.MouseEvent;
20import java.util.Collection;
21import java.util.LinkedHashSet;
22
23import javax.swing.JOptionPane;
24
25import org.openstreetmap.josm.Main;
26import org.openstreetmap.josm.data.Bounds;
27import org.openstreetmap.josm.data.coor.EastNorth;
28import org.openstreetmap.josm.data.osm.Node;
29import org.openstreetmap.josm.data.osm.OsmPrimitive;
30import org.openstreetmap.josm.data.osm.Way;
31import org.openstreetmap.josm.data.osm.WaySegment;
32import org.openstreetmap.josm.gui.MapFrame;
33import org.openstreetmap.josm.gui.MapView;
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.tools.Geometry;
38import org.openstreetmap.josm.tools.ImageProvider;
39import org.openstreetmap.josm.tools.Shortcut;
40
41//// TODO: (list below)
42/* == Functionality ==
43 *
44 * 1. Use selected nodes as split points for the selected ways.
45 *
46 * The ways containing the selected nodes will be split and only the "inner"
47 * parts will be copied
48 *
49 * 2. Enter exact offset
50 *
51 * 3. Improve snapping
52 *
53 * Need at least a setting for step length
54 *
55 * 4. Visual cues could be better
56 *
57 * 5. Cursors (Half-done)
58 *
59 * 6. (long term) Parallelize and adjust offsets of existing ways
60 *
61 * == Code quality ==
62 *
63 * a) The mode, flags, and modifiers might be updated more than necessary.
64 *
65 * Not a performance problem, but better if they where more centralized
66 *
67 * b) Extract generic MapMode services into a super class and/or utility class
68 *
69 * c) Maybe better to simply draw our own source way highlighting?
70 *
71 * Current code doesn't not take into account that ways might been highlighted
72 * by other than us. Don't think that situation should ever happen though.
73 */
74
75/**
76 * MapMode for making parallel ways.
77 *
78 * All calculations are done in projected coordinates.
79 *
80 * @author Ole Jørgen Brønner (olejorgenb)
81 */
82public class ParallelWayAction extends MapMode implements AWTEventListener, MapViewPaintable {
83
84 private static final long serialVersionUID = 1L;
85
86 private enum Mode {
87 dragging, normal
88 }
89
90 //// Preferences and flags
91 // See updateModeLocalPreferences for defaults
92 private Mode mode;
93 private boolean copyTags;
94 private boolean copyTagsDefault;
95
96 private boolean snap;
97 private boolean snapDefault;
98
99 private double snapThreshold;
100
101 private ModifiersSpec snapModifierCombo;
102 private ModifiersSpec copyTagsModifierCombo;
103 private ModifiersSpec addToSelectionModifierCombo;
104 private ModifiersSpec toggleSelectedModifierCombo;
105 private ModifiersSpec setSelectedModifierCombo;
106
107 private int initialMoveDelay;
108
109 private final MapView mv;
110
111 private boolean ctrl;
112 private boolean alt;
113 private boolean shift;
114
115 // Mouse tracking state
116 private Point mousePressedPos;
117 private boolean mouseIsDown;
118 private long mousePressedTime;
119 private boolean mouseHasBeenDragged;
120
121 private WaySegment referenceSegment;
122 private ParallelWays pWays;
123 LinkedHashSet<Way> sourceWays;
124 private EastNorth helperLineStart;
125 private EastNorth helperLineEnd;
126
127 public ParallelWayAction(MapFrame mapFrame) {
128 super(tr("Parallel"), "parallel", tr("Make parallel copies of ways"), Shortcut
129 .registerShortcut("mapmode:parallel", tr("Mode: {0}", tr("Parallel")), KeyEvent.VK_P,
130 Shortcut.GROUP_EDIT, Shortcut.SHIFT_DEFAULT), mapFrame, ImageProvider.getCursor("normal",
131 "parallel"));
132 putValue("help", ht("/Action/Parallel"));
133 mv = mapFrame.mapView;
134 updateModeLocalPreferences();
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"), 0.35);
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
208 snapModifierCombo = new ModifiersSpec(getStringPref("snap-modifier-combo", "?sC"));
209 copyTagsModifierCombo = new ModifiersSpec(getStringPref("copy-tags-modifier-combo", "As?"));
210 addToSelectionModifierCombo = new ModifiersSpec(getStringPref("add-to-selection-modifier-combo", "aSc"));
211 toggleSelectedModifierCombo = new ModifiersSpec(getStringPref("toggle-selection-modifier-combo", "asC"));
212 setSelectedModifierCombo = new ModifiersSpec(getStringPref("set-selection-modifier-combo", "asc"));
213 // @formatter:on
214 }
215
216 @Override
217 public boolean layerIsSupported(Layer layer) {
218 return layer instanceof OsmDataLayer;
219 }
220
221 @Override
222 public void eventDispatched(AWTEvent e) {
223 if (Main.map == null || mv == null || !mv.isActiveLayerDrawable())
224 return;
225
226 // Should only get InputEvents due to the mask in enterMode
227 if (updateModifiersState((InputEvent) e)) {
228 updateStatusLine();
229 updateCursor();
230 }
231 }
232
233 private boolean updateModifiersState(InputEvent e) {
234 boolean oldAlt = alt, oldShift = shift, oldCtrl = ctrl;
235 alt = (e.getModifiers() & (ActionEvent.ALT_MASK | InputEvent.ALT_GRAPH_MASK)) != 0;
236 ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0;
237 shift = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0;
238 boolean changed = (oldAlt != alt || oldShift != shift || oldCtrl != ctrl);
239 return changed;
240 }
241
242 private void updateCursor() {
243 Cursor newCursor = null;
244 switch (mode) {
245 case normal:
246 if (matchesCurrentModifiers(setSelectedModifierCombo)) {
247 newCursor = ImageProvider.getCursor("normal", "parallel");
248 } else if (matchesCurrentModifiers(addToSelectionModifierCombo)) {
249 newCursor = ImageProvider.getCursor("normal", "parallel"); // FIXME
250 } else if (matchesCurrentModifiers(toggleSelectedModifierCombo)) {
251 newCursor = ImageProvider.getCursor("normal", "parallel"); // FIXME
252 } else {
253 // TODO: set to a cursor indicating an error
254 }
255 break;
256 case dragging:
257 if (snap) {
258 // TODO: snapping cursor?
259 newCursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
260 } else {
261 newCursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
262 }
263 }
264 if (newCursor != null) {
265 mv.setNewCursor(newCursor, this);
266 }
267 }
268
269 private void setMode(Mode mode) {
270 this.mode = mode;
271 updateCursor();
272 updateStatusLine();
273 }
274
275 private boolean isValidModifierCombination() {
276 // TODO: implement to give feedback on invalid modifier combination
277 return true;
278 }
279
280 private boolean sanityCheck() {
281 // @formatter:off
282 boolean areWeSane =
283 mv.isActiveLayerVisible() &&
284 mv.isActiveLayerDrawable() &&
285 ((Boolean) this.getValue("active"));
286 // @formatter:on
287 assert (areWeSane); // mad == bad
288 return areWeSane;
289 }
290
291 @Override
292 public void mousePressed(MouseEvent e) {
293 updateModifiersState(e);
294 // Other buttons are off limit, but we still get events.
295 if (e.getButton() != MouseEvent.BUTTON1)
296 return;
297
298 if(sanityCheck() == false)
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);
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);
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 }
399 setMode(Mode.dragging);
400 }
401
402 //// Calculate distance to the reference line
403 EastNorth enp = mv.getEastNorth((int) p.getX(), (int) p.getY());
404 EastNorth nearestPointOnRefLine = Geometry.closestPointToLine(referenceSegment.getFirstNode().getEastNorth(),
405 referenceSegment.getSecondNode().getEastNorth(), enp);
406
407 // Note: d is the distance in _projected units_
408 double d = enp.distance(nearestPointOnRefLine);
409 double realD = mv.getProjection().eastNorth2latlon(enp).greatCircleDistance(mv.getProjection().eastNorth2latlon(nearestPointOnRefLine));
410 double snappedRealD = realD;
411
412 // TODO: abuse of isToTheRightSideOfLine function.
413 boolean toTheRight = Geometry.isToTheRightSideOfLine(referenceSegment.getFirstNode(),
414 referenceSegment.getFirstNode(), referenceSegment.getSecondNode(), new Node(enp));
415
416 if (snap) {
417 // TODO: Very simple snapping
418 // - Snap steps and/or threshold relative to the distance?
419 long closestWholeUnit = Math.round(realD);
420 if (Math.abs(closestWholeUnit - realD) < snapThreshold) {
421 snappedRealD = closestWholeUnit;
422 } else {
423 snappedRealD = closestWholeUnit + Math.signum(closestWholeUnit - realD) * -0.5;
424 }
425 }
426 d = snappedRealD * (d/realD); // convert back to projected distance. (probably ok on small scales)
427 helperLineStart = nearestPointOnRefLine;
428 helperLineEnd = enp;
429 if (toTheRight) {
430 d = -d;
431 }
432 pWays.changeOffset(d);
433
434 Main.map.statusLine.setDist(Math.abs(snappedRealD));
435 Main.map.statusLine.repaint();
436 mv.repaint();
437 }
438
439 private boolean matchesCurrentModifiers(ModifiersSpec spec) {
440 return spec.matchWithKnown(alt, shift, ctrl);
441 }
442
443 @Override
444 public void paint(Graphics2D g, MapView mv, Bounds bbox) {
445 if (mode == Mode.dragging) {
446 // sanity checks
447 if (mv == null)
448 return;
449
450 // FIXME: should clip the line (gets insanely slow when zoomed in on a very long line
451 Stroke refLineStroke = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 10.0f, new float[] {
452 2f, 2f }, 0f);
453 g.setStroke(refLineStroke);
454 g.setColor(Color.RED);
455 Point p1 = mv.getPoint(referenceSegment.getFirstNode().getEastNorth());
456 Point p2 = mv.getPoint(referenceSegment.getSecondNode().getEastNorth());
457 g.drawLine(p1.x, p1.y, p2.x, p2.y);
458
459 Stroke helpLineStroke = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL);
460 g.setStroke(helpLineStroke);
461 g.setColor(Color.RED);
462 p1 = mv.getPoint(helperLineStart);
463 p2 = mv.getPoint(helperLineEnd);
464 g.drawLine(p1.x, p1.y, p2.x, p2.y);
465 }
466 }
467
468 private boolean isModifiersValidForDragMode() {
469 return (!alt && !shift && !ctrl) || matchesCurrentModifiers(snapModifierCombo)
470 || matchesCurrentModifiers(copyTagsModifierCombo);
471 }
472
473 private void updateFlagsOnlyChangeableOnPress() {
474 copyTags = copyTagsDefault != matchesCurrentModifiers(copyTagsModifierCombo);
475 }
476
477 private void updateFlagsChangeableAlways() {
478 snap = snapDefault != matchesCurrentModifiers(snapModifierCombo);
479 }
480
481 //// We keep the source ways and the selection in sync so the user can see the source way's tags
482 private void addSourceWay(Way w) {
483 assert (sourceWays != null);
484 getCurrentDataSet().addSelected(w);
485 w.setHighlighted(true);
486 sourceWays.add(w);
487 }
488
489 private void removeSourceWay(Way w) {
490 assert (sourceWays != null);
491 getCurrentDataSet().clearSelection(w);
492 w.setHighlighted(false);
493 sourceWays.remove(w);
494 }
495
496 private void clearSourceWays() {
497 assert (sourceWays != null);
498 if (sourceWays == null)
499 return;
500 getCurrentDataSet().clearSelection(sourceWays);
501 for (Way w : sourceWays) {
502 w.setHighlighted(false);
503 }
504 sourceWays.clear();
505 }
506
507 private void resetMouseTrackingState() {
508 mouseIsDown = false;
509 mousePressedPos = null;
510 mouseHasBeenDragged = false;
511 }
512
513 // TODO: rename
514 private boolean initParallelWays(Point p, boolean copyTags) {
515 referenceSegment = mv.getNearestWaySegment(p, Way.isUsablePredicate, true);
516 if (referenceSegment == null)
517 return false;
518
519 if (!sourceWays.contains(referenceSegment.way)) {
520 clearSourceWays();
521 addSourceWay(referenceSegment.way);
522 }
523
524 try {
525 int referenceWayIndex = -1;
526 int i = 0;
527 for (Way w : sourceWays) {
528 if (w == referenceSegment.way) {
529 referenceWayIndex = i;
530 break;
531 }
532 i++;
533 }
534 pWays = new ParallelWays(sourceWays, copyTags, referenceWayIndex);
535 pWays.commit();
536 getCurrentDataSet().setSelected(pWays.ways);
537 return true;
538 } catch (IllegalArgumentException e) {
539 // TODO: Not ideal feedback. Maybe changing the cursor could be a good mechanism?
540 JOptionPane.showMessageDialog(
541 Main.parent,
542 tr("ParallelWayAction\n" +
543 "The ways selected must form a simple branchless path"),
544 tr("Make parallel way error"),
545 JOptionPane.INFORMATION_MESSAGE);
546 // The error dialog prevents us from getting the mouseReleased event
547 resetMouseTrackingState();
548 pWays = null;
549 return false;
550 }
551 }
552
553 private String prefKey(String subKey) {
554 return "edit.make-parallel-way-action." + subKey;
555 }
556
557 private String getStringPref(String subKey, String def) {
558 return Main.pref.get(prefKey(subKey), def);
559 }
560
561 private String getStringPref(String subKey) {
562 return getStringPref(subKey, null);
563 }
564}
Note: See TracBrowser for help on using the repository browser.