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

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

fix #8919 - fix memory leaks, including one introduced with r5982

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