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

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

see #15420 - should fix NPE in ParallelWays.copyNode (probably caused by an incomplete or empty way)

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