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

Last change on this file since 17896 was 17896, checked in by simon04, 3 years ago

see #17177 - IWaySegment: encapsulate fields

  • Property svn:eol-style set to native
File size: 24.3 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.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 l) {
215 return isEditableDataLayer(l);
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 MainApplication.getMap().statusLine.setDist(pWays.getWays());
336 }
337
338 setMode(Mode.NORMAL);
339 resetMouseTrackingState();
340 temporaryLayer.invalidate();
341 }
342
343 private static void removeWayHighlighting(Collection<Way> ways) {
344 if (ways == null)
345 return;
346 for (Way w : ways) {
347 w.setHighlighted(false);
348 }
349 }
350
351 @Override
352 public void mouseDragged(MouseEvent e) {
353 // WTF.. the event passed here doesn't have button info?
354 // Since we get this event from other buttons too, we must check that
355 // _BUTTON1_ is down.
356 if (!mouseIsDown)
357 return;
358
359 boolean modifiersChanged = updateModifiersState(e.getModifiersEx());
360 updateFlagsChangeableAlways();
361
362 if (modifiersChanged) {
363 // Since this could be remotely slow, do it conditionally
364 updateStatusLine();
365 updateCursor();
366 }
367
368 if ((System.currentTimeMillis() - mousePressedTime) < INITIAL_MOVE_DELAY.get())
369 return;
370 // Assuming this event only is emitted when the mouse has moved
371 // Setting this after the check above means we tolerate clicks with some movement
372 mouseHasBeenDragged = true;
373
374 if (mode == Mode.NORMAL) {
375 // Should we ensure that the copyTags modifiers are still valid?
376
377 // Important to use mouse position from the press, since the drag
378 // event can come quite late
379 if (!isModifiersValidForDragMode())
380 return;
381 if (!initParallelWays(mousePressedPos, copyTags))
382 return;
383 setMode(Mode.DRAGGING);
384 }
385
386 // Calculate distance to the reference line
387 Point p = e.getPoint();
388 EastNorth enp = mv.getEastNorth((int) p.getX(), (int) p.getY());
389 EastNorth nearestPointOnRefLine = Geometry.closestPointToLine(referenceSegment.getFirstNode().getEastNorth(),
390 referenceSegment.getSecondNode().getEastNorth(), enp);
391
392 // Note: d is the distance in _projected units_
393 double d = enp.distance(nearestPointOnRefLine);
394 double realD = mv.getProjection().eastNorth2latlon(enp).greatCircleDistance(mv.getProjection().eastNorth2latlon(nearestPointOnRefLine));
395 double snappedRealD = realD;
396
397 boolean toTheRight = Geometry.angleIsClockwise(
398 referenceSegment.getFirstNode(), referenceSegment.getSecondNode(), new Node(enp));
399
400 if (snap) {
401 // TODO: Very simple snapping
402 // - Snap steps relative to the distance?
403 double snapDistance;
404 SystemOfMeasurement som = SystemOfMeasurement.getSystemOfMeasurement();
405 if (som.equals(SystemOfMeasurement.CHINESE)) {
406 snapDistance = SNAP_DISTANCE_CHINESE.get() * SystemOfMeasurement.CHINESE.aValue;
407 } else if (som.equals(SystemOfMeasurement.IMPERIAL)) {
408 snapDistance = SNAP_DISTANCE_IMPERIAL.get() * SystemOfMeasurement.IMPERIAL.aValue;
409 } else if (som.equals(SystemOfMeasurement.NAUTICAL_MILE)) {
410 snapDistance = SNAP_DISTANCE_NAUTICAL.get() * SystemOfMeasurement.NAUTICAL_MILE.aValue;
411 } else {
412 snapDistance = SNAP_DISTANCE_METRIC.get(); // Metric system by default
413 }
414 double closestWholeUnit;
415 double modulo = realD % snapDistance;
416 if (modulo < snapDistance/2.0) {
417 closestWholeUnit = realD - modulo;
418 } else {
419 closestWholeUnit = realD + (snapDistance-modulo);
420 }
421 if (Math.abs(closestWholeUnit - realD) < (SNAP_THRESHOLD.get() * snapDistance)) {
422 snappedRealD = closestWholeUnit;
423 } else {
424 snappedRealD = closestWholeUnit + Math.signum(realD - closestWholeUnit) * snapDistance;
425 }
426 }
427 d = snappedRealD * (d/realD); // convert back to projected distance. (probably ok on small scales)
428 helperLineStart = nearestPointOnRefLine;
429 helperLineEnd = enp;
430 if (toTheRight) {
431 d = -d;
432 }
433 pWays.changeOffset(d);
434
435 MapFrame map = MainApplication.getMap();
436 map.statusLine.setDist(Math.abs(snappedRealD));
437 map.statusLine.repaint();
438 temporaryLayer.invalidate();
439 }
440
441 private boolean matchesCurrentModifiers(CachingProperty<Map<Modifier, Boolean>> spec) {
442 return matchesCurrentModifiers(spec.get());
443 }
444
445 private boolean matchesCurrentModifiers(Map<Modifier, Boolean> spec) {
446 EnumSet<Modifier> modifiers = EnumSet.noneOf(Modifier.class);
447 if (ctrl) {
448 modifiers.add(Modifier.CTRL);
449 }
450 if (alt) {
451 modifiers.add(Modifier.ALT);
452 }
453 if (shift) {
454 modifiers.add(Modifier.SHIFT);
455 }
456 return spec.entrySet().stream().allMatch(entry -> modifiers.contains(entry.getKey()) == entry.getValue().booleanValue());
457 }
458
459 private boolean isModifiersValidForDragMode() {
460 return (!alt && !shift && !ctrl) || matchesCurrentModifiers(SNAP_MODIFIER_COMBO)
461 || matchesCurrentModifiers(COPY_TAGS_MODIFIER_COMBO);
462 }
463
464 private void updateFlagsOnlyChangeableOnPress() {
465 copyTags = COPY_TAGS_DEFAULT.get().booleanValue() != matchesCurrentModifiers(COPY_TAGS_MODIFIER_COMBO);
466 }
467
468 private void updateFlagsChangeableAlways() {
469 snap = SNAP_DEFAULT.get().booleanValue() != matchesCurrentModifiers(SNAP_MODIFIER_COMBO);
470 }
471
472 // We keep the source ways and the selection in sync so the user can see the source way's tags
473 private void addSourceWay(Way w) {
474 assert sourceWays != null;
475 getLayerManager().getEditDataSet().addSelected(w);
476 w.setHighlighted(true);
477 sourceWays.add(w);
478 }
479
480 private void removeSourceWay(Way w) {
481 assert sourceWays != null;
482 getLayerManager().getEditDataSet().clearSelection(w);
483 w.setHighlighted(false);
484 sourceWays.remove(w);
485 }
486
487 private void clearSourceWays() {
488 assert sourceWays != null;
489 getLayerManager().getEditDataSet().clearSelection(sourceWays);
490 for (Way w : sourceWays) {
491 w.setHighlighted(false);
492 }
493 sourceWays.clear();
494 }
495
496 private void resetMouseTrackingState() {
497 mouseIsDown = false;
498 mousePressedPos = null;
499 mouseHasBeenDragged = false;
500 }
501
502 // TODO: rename
503 private boolean initParallelWays(Point p, boolean copyTags) {
504 referenceSegment = mv.getNearestWaySegment(p, OsmPrimitive::isUsable, true);
505 if (referenceSegment == null)
506 return false;
507
508 sourceWays.removeIf(w -> w.isIncomplete() || w.isEmpty());
509
510 if (!sourceWays.contains(referenceSegment.getWay())) {
511 clearSourceWays();
512 addSourceWay(referenceSegment.getWay());
513 }
514
515 try {
516 int referenceWayIndex = -1;
517 int i = 0;
518 for (Way w : sourceWays) {
519 if (w == referenceSegment.getWay()) {
520 referenceWayIndex = i;
521 break;
522 }
523 i++;
524 }
525 pWays = new ParallelWays(sourceWays, copyTags, referenceWayIndex);
526 pWays.commit();
527 getLayerManager().getEditDataSet().setSelected(pWays.getWays());
528 return true;
529 } catch (IllegalArgumentException e) {
530 Logging.debug(e);
531 new Notification(tr("ParallelWayAction\n" +
532 "The ways selected must form a simple branchless path"))
533 .setIcon(JOptionPane.INFORMATION_MESSAGE)
534 .show();
535 // The error dialog prevents us from getting the mouseReleased event
536 resetMouseTrackingState();
537 pWays = null;
538 return false;
539 }
540 }
541
542 private static String prefKey(String subKey) {
543 return "edit.make-parallel-way-action." + subKey;
544 }
545
546 /**
547 * A property that holds the keyboard modifiers.
548 * @author Michael Zangl
549 * @since 10869
550 */
551 private static class KeyboardModifiersProperty extends AbstractToStringProperty<Map<Modifier, Boolean>> {
552
553 KeyboardModifiersProperty(String key, String defaultValue) {
554 super(key, createFromString(defaultValue));
555 }
556
557 KeyboardModifiersProperty(String key, Map<Modifier, Boolean> defaultValue) {
558 super(key, defaultValue);
559 }
560
561 @Override
562 protected String toString(Map<Modifier, Boolean> t) {
563 StringBuilder sb = new StringBuilder();
564 for (Modifier mod : Modifier.values()) {
565 Boolean val = t.get(mod);
566 if (val == null) {
567 sb.append('?');
568 } else if (val) {
569 sb.append(Character.toUpperCase(mod.shortChar));
570 } else {
571 sb.append(mod.shortChar);
572 }
573 }
574 return sb.toString();
575 }
576
577 @Override
578 protected Map<Modifier, Boolean> fromString(String string) {
579 return createFromString(string);
580 }
581
582 private static Map<Modifier, Boolean> createFromString(String string) {
583 Map<Modifier, Boolean> ret = new EnumMap<>(Modifier.class);
584 for (char c : string.toCharArray()) {
585 if (c == '?') {
586 continue;
587 }
588 Optional<Modifier> mod = Modifier.findWithShortCode(c);
589 if (mod.isPresent()) {
590 ret.put(mod.get(), Character.isUpperCase(c));
591 } else {
592 Logging.debug("Ignoring unknown modifier {0}", c);
593 }
594 }
595 return Collections.unmodifiableMap(ret);
596 }
597 }
598
599 enum Modifier {
600 CTRL('c'),
601 ALT('a'),
602 SHIFT('s');
603
604 private final char shortChar;
605
606 Modifier(char shortChar) {
607 this.shortChar = Character.toLowerCase(shortChar);
608 }
609
610 /**
611 * Find the modifier with the given short code
612 * @param charCode The short code
613 * @return The modifier
614 */
615 public static Optional<Modifier> findWithShortCode(int charCode) {
616 return Stream.of(values()).filter(m -> m.shortChar == Character.toLowerCase(charCode)).findAny();
617 }
618 }
619
620 private class ParallelWayLayer extends AbstractMapViewPaintable {
621 @Override
622 public void paint(Graphics2D g, MapView mv, Bounds bbox) {
623 if (mode == Mode.DRAGGING) {
624 CheckParameterUtil.ensureParameterNotNull(mv, "mv");
625
626 Color mainColor = MAIN_COLOR.get();
627 g.setStroke(REF_LINE_STROKE.get());
628 g.setColor(mainColor);
629 MapViewPath line = new MapViewPath(mv);
630 line.moveTo(referenceSegment.getFirstNode());
631 line.lineTo(referenceSegment.getSecondNode());
632 g.draw(line.computeClippedLine(g.getStroke()));
633
634 g.setStroke(HELPER_LINE_STROKE.get());
635 g.setColor(mainColor);
636 line = new MapViewPath(mv);
637 line.moveTo(helperLineStart);
638 line.lineTo(helperLineEnd);
639 g.draw(line.computeClippedLine(g.getStroke()));
640 }
641 }
642 }
643}
Note: See TracBrowser for help on using the repository browser.