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

Last change on this file since 17108 was 17108, checked in by GerdP, 4 years ago

fix #19887: Parallel ways tool: bottom toolbar unusable to view offset measurement

  • implememt new method MapStatus.setAutoLength()
  • ParallelWayAction calls it with false in enterMode()
  • call it in MapMode.enterMode() to make sure that it is reenabled even if ParallelWayAction.exitMode() is not called.
  • Property svn:eol-style set to native
File size: 24.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.BasicStroke;
9import java.awt.Color;
10import java.awt.Cursor;
11import java.awt.Graphics2D;
12import java.awt.Point;
13import java.awt.event.KeyEvent;
14import java.awt.event.MouseEvent;
15import java.util.Collection;
16import java.util.Collections;
17import java.util.EnumMap;
18import java.util.EnumSet;
19import java.util.LinkedHashSet;
20import java.util.Map;
21import java.util.Optional;
22import java.util.Set;
23import java.util.stream.Stream;
24
25import javax.swing.JOptionPane;
26
27import org.openstreetmap.josm.data.Bounds;
28import org.openstreetmap.josm.data.SystemOfMeasurement;
29import org.openstreetmap.josm.data.coor.EastNorth;
30import org.openstreetmap.josm.data.osm.Node;
31import org.openstreetmap.josm.data.osm.OsmPrimitive;
32import org.openstreetmap.josm.data.osm.Way;
33import org.openstreetmap.josm.data.osm.WaySegment;
34import org.openstreetmap.josm.data.preferences.AbstractToStringProperty;
35import org.openstreetmap.josm.data.preferences.BooleanProperty;
36import org.openstreetmap.josm.data.preferences.CachingProperty;
37import org.openstreetmap.josm.data.preferences.DoubleProperty;
38import org.openstreetmap.josm.data.preferences.IntegerProperty;
39import org.openstreetmap.josm.data.preferences.NamedColorProperty;
40import org.openstreetmap.josm.data.preferences.StrokeProperty;
41import org.openstreetmap.josm.gui.MainApplication;
42import org.openstreetmap.josm.gui.MapFrame;
43import org.openstreetmap.josm.gui.MapView;
44import org.openstreetmap.josm.gui.Notification;
45import org.openstreetmap.josm.gui.draw.MapViewPath;
46import org.openstreetmap.josm.gui.layer.AbstractMapViewPaintable;
47import org.openstreetmap.josm.gui.layer.Layer;
48import org.openstreetmap.josm.gui.util.ModifierExListener;
49import org.openstreetmap.josm.tools.CheckParameterUtil;
50import org.openstreetmap.josm.tools.Geometry;
51import org.openstreetmap.josm.tools.ImageProvider;
52import org.openstreetmap.josm.tools.Logging;
53import org.openstreetmap.josm.tools.Shortcut;
54
55/**
56 * MapMode for making parallel ways.
57 *
58 * All calculations are done in projected coordinates.
59 *
60 * TODO:
61 * == Functionality ==
62 *
63 * 1. Use selected nodes as split points for the selected ways.
64 *
65 * The ways containing the selected nodes will be split and only the "inner"
66 * parts will be copied
67 *
68 * 2. Enter exact offset
69 *
70 * 3. Improve snapping
71 *
72 * 4. Visual cues could be better
73 *
74 * 5. (long term) Parallelize and adjust offsets of existing ways
75 *
76 * == Code quality ==
77 *
78 * a) The mode, flags, and modifiers might be updated more than necessary.
79 *
80 * Not a performance problem, but better if they where more centralized
81 *
82 * b) Extract generic MapMode services into a super class and/or utility class
83 *
84 * c) Maybe better to simply draw our own source way highlighting?
85 *
86 * Current code doesn't not take into account that ways might been highlighted
87 * by other than us. Don't think that situation should ever happen though.
88 *
89 * @author Ole Jørgen Brønner (olejorgenb)
90 */
91public class ParallelWayAction extends MapMode implements ModifierExListener {
92
93 private static final CachingProperty<BasicStroke> HELPER_LINE_STROKE = new StrokeProperty(prefKey("stroke.hepler-line"), "1").cached();
94 private static final CachingProperty<BasicStroke> REF_LINE_STROKE = new StrokeProperty(prefKey("stroke.ref-line"), "2 2 3").cached();
95
96 // @formatter:off
97 // CHECKSTYLE.OFF: SingleSpaceSeparator
98 private static final CachingProperty<Double> SNAP_THRESHOLD = new DoubleProperty(prefKey("snap-threshold-percent"), 0.70).cached();
99 private static final CachingProperty<Boolean> SNAP_DEFAULT = new BooleanProperty(prefKey("snap-default"), true).cached();
100 private static final CachingProperty<Boolean> COPY_TAGS_DEFAULT = new BooleanProperty(prefKey("copy-tags-default"), true).cached();
101 private static final CachingProperty<Integer> INITIAL_MOVE_DELAY = new IntegerProperty(prefKey("initial-move-delay"), 200).cached();
102 private static final CachingProperty<Double> SNAP_DISTANCE_METRIC = new DoubleProperty(prefKey("snap-distance-metric"), 0.5).cached();
103 private static final CachingProperty<Double> SNAP_DISTANCE_IMPERIAL = new DoubleProperty(prefKey("snap-distance-imperial"), 1).cached();
104 private static final CachingProperty<Double> SNAP_DISTANCE_CHINESE = new DoubleProperty(prefKey("snap-distance-chinese"), 1).cached();
105 private static final CachingProperty<Double> SNAP_DISTANCE_NAUTICAL = new DoubleProperty(prefKey("snap-distance-nautical"), 0.1).cached();
106 private static final CachingProperty<Color> MAIN_COLOR = new NamedColorProperty(marktr("make parallel helper line"), Color.RED).cached();
107
108 private static final CachingProperty<Map<Modifier, Boolean>> SNAP_MODIFIER_COMBO
109 = new KeyboardModifiersProperty(prefKey("snap-modifier-combo"), "?sC").cached();
110 private static final CachingProperty<Map<Modifier, Boolean>> COPY_TAGS_MODIFIER_COMBO
111 = new KeyboardModifiersProperty(prefKey("copy-tags-modifier-combo"), "As?").cached();
112 private static final CachingProperty<Map<Modifier, Boolean>> ADD_TO_SELECTION_MODIFIER_COMBO
113 = new KeyboardModifiersProperty(prefKey("add-to-selection-modifier-combo"), "aSc").cached();
114 private static final CachingProperty<Map<Modifier, Boolean>> TOGGLE_SELECTED_MODIFIER_COMBO
115 = new KeyboardModifiersProperty(prefKey("toggle-selection-modifier-combo"), "asC").cached();
116 private static final CachingProperty<Map<Modifier, Boolean>> SET_SELECTED_MODIFIER_COMBO
117 = new KeyboardModifiersProperty(prefKey("set-selection-modifier-combo"), "asc").cached();
118 // CHECKSTYLE.ON: SingleSpaceSeparator
119 // @formatter:on
120
121 enum Mode {
122 DRAGGING, NORMAL
123 }
124
125 //// Preferences and flags
126 // See updateModeLocalPreferences for defaults
127 private Mode mode;
128 private boolean copyTags;
129
130 private boolean snap;
131
132 private final MapView mv;
133
134 // Mouse tracking state
135 private Point mousePressedPos;
136 private boolean mouseIsDown;
137 private long mousePressedTime;
138 private boolean mouseHasBeenDragged;
139
140 private transient WaySegment referenceSegment;
141 private transient ParallelWays pWays;
142 private transient Set<Way> sourceWays;
143 private EastNorth helperLineStart;
144 private EastNorth helperLineEnd;
145
146 private final ParallelWayLayer temporaryLayer = new ParallelWayLayer();
147
148 /**
149 * Constructs a new {@code ParallelWayAction}.
150 * @param mapFrame Map frame
151 */
152 public ParallelWayAction(MapFrame mapFrame) {
153 super(tr("Parallel"), "parallel", tr("Make parallel copies of ways"),
154 Shortcut.registerShortcut("mapmode:parallel", tr("Mode: {0}",
155 tr("Parallel")), KeyEvent.VK_P, Shortcut.SHIFT),
156 ImageProvider.getCursor("normal", "parallel"));
157 setHelpId(ht("/Action/Parallel"));
158 mv = mapFrame.mapView;
159 }
160
161 @Override
162 public void enterMode() {
163 // super.enterMode() updates the status line and cursor so we need our state to be set correctly
164 setMode(Mode.NORMAL);
165 pWays = null;
166 super.enterMode();
167
168 // #19887: overwrite default: we want to show the distance to the original way
169 MainApplication.getMap().statusLine.setAutoLength(false);
170
171 mv.addMouseListener(this);
172 mv.addMouseMotionListener(this);
173 mv.addTemporaryLayer(temporaryLayer);
174
175 // Needed to update the mouse cursor if modifiers are changed when the mouse is motionless
176 MainApplication.getMap().keyDetector.addModifierExListener(this);
177 sourceWays = new LinkedHashSet<>(getLayerManager().getEditDataSet().getSelectedWays());
178 for (Way w : sourceWays) {
179 w.setHighlighted(true);
180 }
181 }
182
183 @Override
184 public void exitMode() {
185 super.exitMode();
186 mv.removeMouseListener(this);
187 mv.removeMouseMotionListener(this);
188 mv.removeTemporaryLayer(temporaryLayer);
189 MapFrame map = MainApplication.getMap();
190 map.statusLine.setDist(-1);
191 map.statusLine.setAutoLength(true);
192 map.keyDetector.removeModifierExListener(this);
193 removeWayHighlighting(sourceWays);
194 pWays = null;
195 sourceWays = null;
196 referenceSegment = null;
197 }
198
199 @Override
200 public String getModeHelpText() {
201 // TODO: add more detailed feedback based on modifier state.
202 // TODO: dynamic messages based on preferences. (Could be problematic translation wise)
203 switch (mode) {
204 case NORMAL:
205 // CHECKSTYLE.OFF: LineLength
206 return tr("Select ways as in Select mode. Drag selected ways or a single way to create a parallel copy (Alt toggles tag preservation)");
207 // CHECKSTYLE.ON: LineLength
208 case DRAGGING:
209 return tr("Hold Ctrl to toggle snapping");
210 }
211 return ""; // impossible ..
212 }
213
214 @Override
215 public boolean layerIsSupported(Layer l) {
216 return isEditableDataLayer(l);
217 }
218
219 @Override
220 public void modifiersExChanged(int modifiers) {
221 if (MainApplication.getMap() == null || mv == null || !mv.isActiveLayerDrawable())
222 return;
223
224 // Should only get InputEvents due to the mask in enterMode
225 if (updateModifiersState(modifiers)) {
226 updateStatusLine();
227 updateCursor();
228 }
229 }
230
231 private boolean updateModifiersState(int modifiers) {
232 boolean oldAlt = alt, oldShift = shift, oldCtrl = ctrl;
233 updateKeyModifiersEx(modifiers);
234 return oldAlt != alt || oldShift != shift || oldCtrl != ctrl;
235 }
236
237 private void updateCursor() {
238 Cursor newCursor = null;
239 switch (mode) {
240 case NORMAL:
241 if (matchesCurrentModifiers(SET_SELECTED_MODIFIER_COMBO)) {
242 newCursor = ImageProvider.getCursor("normal", "parallel");
243 } else if (matchesCurrentModifiers(ADD_TO_SELECTION_MODIFIER_COMBO)) {
244 newCursor = ImageProvider.getCursor("normal", "parallel_add");
245 } else if (matchesCurrentModifiers(TOGGLE_SELECTED_MODIFIER_COMBO)) {
246 newCursor = ImageProvider.getCursor("normal", "parallel_remove");
247 }
248 break;
249 case DRAGGING:
250 newCursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
251 break;
252 default: throw new AssertionError();
253 }
254 if (newCursor != null) {
255 mv.setNewCursor(newCursor, this);
256 }
257 }
258
259 private void setMode(Mode mode) {
260 this.mode = mode;
261 updateCursor();
262 updateStatusLine();
263 }
264
265 private boolean sanityCheck() {
266 // @formatter:off
267 boolean areWeSane =
268 mv.isActiveLayerVisible() &&
269 mv.isActiveLayerDrawable() &&
270 ((Boolean) this.getValue("active"));
271 // @formatter:on
272 assert areWeSane; // mad == bad
273 return areWeSane;
274 }
275
276 @Override
277 public void mousePressed(MouseEvent e) {
278 requestFocusInMapView();
279 updateModifiersState(e.getModifiersEx());
280 // Other buttons are off limit, but we still get events.
281 if (e.getButton() != MouseEvent.BUTTON1)
282 return;
283
284 if (!sanityCheck())
285 return;
286
287 updateFlagsOnlyChangeableOnPress();
288 updateFlagsChangeableAlways();
289
290 // Since the created way is left selected, we need to unselect again here
291 if (pWays != null && pWays.getWays() != null) {
292 getLayerManager().getEditDataSet().clearSelection(pWays.getWays());
293 pWays = null;
294 }
295
296 mouseIsDown = true;
297 mousePressedPos = e.getPoint();
298 mousePressedTime = System.currentTimeMillis();
299
300 }
301
302 @Override
303 public void mouseReleased(MouseEvent e) {
304 updateModifiersState(e.getModifiersEx());
305 // Other buttons are off limit, but we still get events.
306 if (e.getButton() != MouseEvent.BUTTON1)
307 return;
308
309 if (!mouseHasBeenDragged) {
310 // use point from press or click event? (or are these always the same)
311 Way nearestWay = mv.getNearestWay(e.getPoint(), OsmPrimitive::isSelectable);
312 if (nearestWay == null) {
313 if (matchesCurrentModifiers(SET_SELECTED_MODIFIER_COMBO)) {
314 clearSourceWays();
315 }
316 resetMouseTrackingState();
317 return;
318 }
319 boolean isSelected = nearestWay.isSelected();
320 if (matchesCurrentModifiers(ADD_TO_SELECTION_MODIFIER_COMBO)) {
321 if (!isSelected) {
322 addSourceWay(nearestWay);
323 }
324 } else if (matchesCurrentModifiers(TOGGLE_SELECTED_MODIFIER_COMBO)) {
325 if (isSelected) {
326 removeSourceWay(nearestWay);
327 } else {
328 addSourceWay(nearestWay);
329 }
330 } else if (matchesCurrentModifiers(SET_SELECTED_MODIFIER_COMBO)) {
331 clearSourceWays();
332 addSourceWay(nearestWay);
333 } // else -> invalid modifier combination
334 } else if (mode == Mode.DRAGGING) {
335 clearSourceWays();
336 MainApplication.getMap().statusLine.setDist(pWays.getWays());
337 }
338
339 setMode(Mode.NORMAL);
340 resetMouseTrackingState();
341 temporaryLayer.invalidate();
342 }
343
344 private static void removeWayHighlighting(Collection<Way> ways) {
345 if (ways == null)
346 return;
347 for (Way w : ways) {
348 w.setHighlighted(false);
349 }
350 }
351
352 @Override
353 public void mouseDragged(MouseEvent e) {
354 // WTF.. the event passed here doesn't have button info?
355 // Since we get this event from other buttons too, we must check that
356 // _BUTTON1_ is down.
357 if (!mouseIsDown)
358 return;
359
360 boolean modifiersChanged = updateModifiersState(e.getModifiersEx());
361 updateFlagsChangeableAlways();
362
363 if (modifiersChanged) {
364 // Since this could be remotely slow, do it conditionally
365 updateStatusLine();
366 updateCursor();
367 }
368
369 if ((System.currentTimeMillis() - mousePressedTime) < INITIAL_MOVE_DELAY.get())
370 return;
371 // Assuming this event only is emitted when the mouse has moved
372 // Setting this after the check above means we tolerate clicks with some movement
373 mouseHasBeenDragged = true;
374
375 if (mode == Mode.NORMAL) {
376 // Should we ensure that the copyTags modifiers are still valid?
377
378 // Important to use mouse position from the press, since the drag
379 // event can come quite late
380 if (!isModifiersValidForDragMode())
381 return;
382 if (!initParallelWays(mousePressedPos, copyTags))
383 return;
384 setMode(Mode.DRAGGING);
385 }
386
387 // Calculate distance to the reference line
388 Point p = e.getPoint();
389 EastNorth enp = mv.getEastNorth((int) p.getX(), (int) p.getY());
390 EastNorth nearestPointOnRefLine = Geometry.closestPointToLine(referenceSegment.getFirstNode().getEastNorth(),
391 referenceSegment.getSecondNode().getEastNorth(), enp);
392
393 // Note: d is the distance in _projected units_
394 double d = enp.distance(nearestPointOnRefLine);
395 double realD = mv.getProjection().eastNorth2latlon(enp).greatCircleDistance(mv.getProjection().eastNorth2latlon(nearestPointOnRefLine));
396 double snappedRealD = realD;
397
398 boolean toTheRight = Geometry.angleIsClockwise(
399 referenceSegment.getFirstNode(), referenceSegment.getSecondNode(), new Node(enp));
400
401 if (snap) {
402 // TODO: Very simple snapping
403 // - Snap steps relative to the distance?
404 double snapDistance;
405 SystemOfMeasurement som = SystemOfMeasurement.getSystemOfMeasurement();
406 if (som.equals(SystemOfMeasurement.CHINESE)) {
407 snapDistance = SNAP_DISTANCE_CHINESE.get() * SystemOfMeasurement.CHINESE.aValue;
408 } else if (som.equals(SystemOfMeasurement.IMPERIAL)) {
409 snapDistance = SNAP_DISTANCE_IMPERIAL.get() * SystemOfMeasurement.IMPERIAL.aValue;
410 } else if (som.equals(SystemOfMeasurement.NAUTICAL_MILE)) {
411 snapDistance = SNAP_DISTANCE_NAUTICAL.get() * SystemOfMeasurement.NAUTICAL_MILE.aValue;
412 } else {
413 snapDistance = SNAP_DISTANCE_METRIC.get(); // Metric system by default
414 }
415 double closestWholeUnit;
416 double modulo = realD % snapDistance;
417 if (modulo < snapDistance/2.0) {
418 closestWholeUnit = realD - modulo;
419 } else {
420 closestWholeUnit = realD + (snapDistance-modulo);
421 }
422 if (Math.abs(closestWholeUnit - realD) < (SNAP_THRESHOLD.get() * snapDistance)) {
423 snappedRealD = closestWholeUnit;
424 } else {
425 snappedRealD = closestWholeUnit + Math.signum(realD - closestWholeUnit) * snapDistance;
426 }
427 }
428 d = snappedRealD * (d/realD); // convert back to projected distance. (probably ok on small scales)
429 helperLineStart = nearestPointOnRefLine;
430 helperLineEnd = enp;
431 if (toTheRight) {
432 d = -d;
433 }
434 pWays.changeOffset(d);
435
436 MapFrame map = MainApplication.getMap();
437 map.statusLine.setDist(Math.abs(snappedRealD));
438 map.statusLine.repaint();
439 temporaryLayer.invalidate();
440 }
441
442 private boolean matchesCurrentModifiers(CachingProperty<Map<Modifier, Boolean>> spec) {
443 return matchesCurrentModifiers(spec.get());
444 }
445
446 private boolean matchesCurrentModifiers(Map<Modifier, Boolean> spec) {
447 EnumSet<Modifier> modifiers = EnumSet.noneOf(Modifier.class);
448 if (ctrl) {
449 modifiers.add(Modifier.CTRL);
450 }
451 if (alt) {
452 modifiers.add(Modifier.ALT);
453 }
454 if (shift) {
455 modifiers.add(Modifier.SHIFT);
456 }
457 return spec.entrySet().stream().allMatch(entry -> modifiers.contains(entry.getKey()) == entry.getValue().booleanValue());
458 }
459
460 private boolean isModifiersValidForDragMode() {
461 return (!alt && !shift && !ctrl) || matchesCurrentModifiers(SNAP_MODIFIER_COMBO)
462 || matchesCurrentModifiers(COPY_TAGS_MODIFIER_COMBO);
463 }
464
465 private void updateFlagsOnlyChangeableOnPress() {
466 copyTags = COPY_TAGS_DEFAULT.get().booleanValue() != matchesCurrentModifiers(COPY_TAGS_MODIFIER_COMBO);
467 }
468
469 private void updateFlagsChangeableAlways() {
470 snap = SNAP_DEFAULT.get().booleanValue() != matchesCurrentModifiers(SNAP_MODIFIER_COMBO);
471 }
472
473 // We keep the source ways and the selection in sync so the user can see the source way's tags
474 private void addSourceWay(Way w) {
475 assert sourceWays != null;
476 getLayerManager().getEditDataSet().addSelected(w);
477 w.setHighlighted(true);
478 sourceWays.add(w);
479 }
480
481 private void removeSourceWay(Way w) {
482 assert sourceWays != null;
483 getLayerManager().getEditDataSet().clearSelection(w);
484 w.setHighlighted(false);
485 sourceWays.remove(w);
486 }
487
488 private void clearSourceWays() {
489 assert sourceWays != null;
490 getLayerManager().getEditDataSet().clearSelection(sourceWays);
491 for (Way w : sourceWays) {
492 w.setHighlighted(false);
493 }
494 sourceWays.clear();
495 }
496
497 private void resetMouseTrackingState() {
498 mouseIsDown = false;
499 mousePressedPos = null;
500 mouseHasBeenDragged = false;
501 }
502
503 // TODO: rename
504 private boolean initParallelWays(Point p, boolean copyTags) {
505 referenceSegment = mv.getNearestWaySegment(p, OsmPrimitive::isUsable, true);
506 if (referenceSegment == null)
507 return false;
508
509 sourceWays.removeIf(w -> w.isIncomplete() || w.isEmpty());
510
511 if (!sourceWays.contains(referenceSegment.way)) {
512 clearSourceWays();
513 addSourceWay(referenceSegment.way);
514 }
515
516 try {
517 int referenceWayIndex = -1;
518 int i = 0;
519 for (Way w : sourceWays) {
520 if (w == referenceSegment.way) {
521 referenceWayIndex = i;
522 break;
523 }
524 i++;
525 }
526 pWays = new ParallelWays(sourceWays, copyTags, referenceWayIndex);
527 pWays.commit();
528 getLayerManager().getEditDataSet().setSelected(pWays.getWays());
529 return true;
530 } catch (IllegalArgumentException e) {
531 Logging.debug(e);
532 new Notification(tr("ParallelWayAction\n" +
533 "The ways selected must form a simple branchless path"))
534 .setIcon(JOptionPane.INFORMATION_MESSAGE)
535 .show();
536 // The error dialog prevents us from getting the mouseReleased event
537 resetMouseTrackingState();
538 pWays = null;
539 return false;
540 }
541 }
542
543 private static String prefKey(String subKey) {
544 return "edit.make-parallel-way-action." + subKey;
545 }
546
547 /**
548 * A property that holds the keyboard modifiers.
549 * @author Michael Zangl
550 * @since 10869
551 */
552 private static class KeyboardModifiersProperty extends AbstractToStringProperty<Map<Modifier, Boolean>> {
553
554 KeyboardModifiersProperty(String key, String defaultValue) {
555 super(key, createFromString(defaultValue));
556 }
557
558 KeyboardModifiersProperty(String key, Map<Modifier, Boolean> defaultValue) {
559 super(key, defaultValue);
560 }
561
562 @Override
563 protected String toString(Map<Modifier, Boolean> t) {
564 StringBuilder sb = new StringBuilder();
565 for (Modifier mod : Modifier.values()) {
566 Boolean val = t.get(mod);
567 if (val == null) {
568 sb.append('?');
569 } else if (val) {
570 sb.append(Character.toUpperCase(mod.shortChar));
571 } else {
572 sb.append(mod.shortChar);
573 }
574 }
575 return sb.toString();
576 }
577
578 @Override
579 protected Map<Modifier, Boolean> fromString(String string) {
580 return createFromString(string);
581 }
582
583 private static Map<Modifier, Boolean> createFromString(String string) {
584 Map<Modifier, Boolean> ret = new EnumMap<>(Modifier.class);
585 for (char c : string.toCharArray()) {
586 if (c == '?') {
587 continue;
588 }
589 Optional<Modifier> mod = Modifier.findWithShortCode(c);
590 if (mod.isPresent()) {
591 ret.put(mod.get(), Character.isUpperCase(c));
592 } else {
593 Logging.debug("Ignoring unknown modifier {0}", c);
594 }
595 }
596 return Collections.unmodifiableMap(ret);
597 }
598 }
599
600 enum Modifier {
601 CTRL('c'),
602 ALT('a'),
603 SHIFT('s');
604
605 private final char shortChar;
606
607 Modifier(char shortChar) {
608 this.shortChar = Character.toLowerCase(shortChar);
609 }
610
611 /**
612 * Find the modifier with the given short code
613 * @param charCode The short code
614 * @return The modifier
615 */
616 public static Optional<Modifier> findWithShortCode(int charCode) {
617 return Stream.of(values()).filter(m -> m.shortChar == Character.toLowerCase(charCode)).findAny();
618 }
619 }
620
621 private class ParallelWayLayer extends AbstractMapViewPaintable {
622 @Override
623 public void paint(Graphics2D g, MapView mv, Bounds bbox) {
624 if (mode == Mode.DRAGGING) {
625 CheckParameterUtil.ensureParameterNotNull(mv, "mv");
626
627 Color mainColor = MAIN_COLOR.get();
628 g.setStroke(REF_LINE_STROKE.get());
629 g.setColor(mainColor);
630 MapViewPath line = new MapViewPath(mv);
631 line.moveTo(referenceSegment.getFirstNode());
632 line.lineTo(referenceSegment.getSecondNode());
633 g.draw(line.computeClippedLine(g.getStroke()));
634
635 g.setStroke(HELPER_LINE_STROKE.get());
636 g.setColor(mainColor);
637 line = new MapViewPath(mv);
638 line.moveTo(helperLineStart);
639 line.lineTo(helperLineEnd);
640 g.draw(line.computeClippedLine(g.getStroke()));
641 }
642 }
643 }
644}
Note: See TracBrowser for help on using the repository browser.