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

Last change on this file was 19307, checked in by taylor.smock, 16 months ago

Fix most new PMD issues

It would be better to use the newer switch syntax introduced in Java 14 (JEP 361),
but we currently target Java 11+. When we move to Java 17, this should be
reverted and the newer switch syntax should be used.

  • Property svn:eol-style set to native
File size: 24.6 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.ArrayList;
16import java.util.Collection;
17import java.util.Collections;
18import java.util.EnumMap;
19import java.util.EnumSet;
20import java.util.LinkedHashSet;
21import java.util.List;
22import java.util.Map;
23import java.util.Optional;
24import java.util.Set;
25import java.util.stream.Stream;
26
27import javax.swing.JOptionPane;
28
29import org.openstreetmap.josm.data.Bounds;
30import org.openstreetmap.josm.data.SystemOfMeasurement;
31import org.openstreetmap.josm.data.coor.EastNorth;
32import org.openstreetmap.josm.data.coor.ILatLon;
33import org.openstreetmap.josm.data.osm.Node;
34import org.openstreetmap.josm.data.osm.OsmPrimitive;
35import org.openstreetmap.josm.data.osm.Way;
36import org.openstreetmap.josm.data.osm.WaySegment;
37import org.openstreetmap.josm.data.preferences.AbstractToStringProperty;
38import org.openstreetmap.josm.data.preferences.BooleanProperty;
39import org.openstreetmap.josm.data.preferences.CachingProperty;
40import org.openstreetmap.josm.data.preferences.DoubleProperty;
41import org.openstreetmap.josm.data.preferences.IntegerProperty;
42import org.openstreetmap.josm.data.preferences.NamedColorProperty;
43import org.openstreetmap.josm.data.preferences.StrokeProperty;
44import org.openstreetmap.josm.gui.MainApplication;
45import org.openstreetmap.josm.gui.MapFrame;
46import org.openstreetmap.josm.gui.MapView;
47import org.openstreetmap.josm.gui.Notification;
48import org.openstreetmap.josm.gui.draw.MapViewPath;
49import org.openstreetmap.josm.gui.layer.AbstractMapViewPaintable;
50import org.openstreetmap.josm.gui.layer.Layer;
51import org.openstreetmap.josm.gui.util.ModifierExListener;
52import org.openstreetmap.josm.tools.CheckParameterUtil;
53import org.openstreetmap.josm.tools.Geometry;
54import org.openstreetmap.josm.tools.ImageProvider;
55import org.openstreetmap.josm.tools.Logging;
56import org.openstreetmap.josm.tools.Shortcut;
57
58/**
59 * MapMode for making parallel ways.
60 * <p>
61 * All calculations are done in projected coordinates.
62 * <p>
63 * TODO:
64 * <p>
65 * == Functionality ==
66 * <ol>
67 * <li>Use selected nodes as split points for the selected ways.
68 * <p>
69 * The ways containing the selected nodes will be split and only the "inner"
70 * parts will be copied</li>
71 * <li>Enter exact offset</li>
72 * <li>Improve snapping</li>
73 * <li>Visual cues could be better</li>
74 * <li>(long term) Parallelize and adjust offsets of existing ways</li>
75 * </ol>
76 * == Code quality ==
77 * <ol type="a">
78 * <li>The mode, flags, and modifiers might be updated more than necessary.
79 * <p>
80 * Not a performance problem, but better if they where more centralized</li>
81 * <li>Extract generic MapMode services into a super class and/or utility class</li>
82 * <li>Maybe better to simply draw our own source way highlighting?</li>
83 * </ol>
84 * Current code doesn't not take into account that ways might been highlighted
85 * by other than us. Don't think that situation should ever happen though.
86 *
87 * @author Ole Jørgen Brønner (olejorgenb)
88 */
89public class ParallelWayAction extends MapMode implements ModifierExListener {
90
91 private static final CachingProperty<BasicStroke> HELPER_LINE_STROKE = new StrokeProperty(prefKey("stroke.hepler-line"), "1").cached();
92 private static final CachingProperty<BasicStroke> REF_LINE_STROKE = new StrokeProperty(prefKey("stroke.ref-line"), "2 2 3").cached();
93
94 // @formatter:off
95 // CHECKSTYLE.OFF: SingleSpaceSeparator
96 private static final CachingProperty<Double> SNAP_THRESHOLD = new DoubleProperty(prefKey("snap-threshold-percent"), 0.70).cached();
97 private static final CachingProperty<Boolean> SNAP_DEFAULT = new BooleanProperty(prefKey("snap-default"), true).cached();
98 private static final CachingProperty<Boolean> COPY_TAGS_DEFAULT = new BooleanProperty(prefKey("copy-tags-default"), true).cached();
99 private static final CachingProperty<Integer> INITIAL_MOVE_DELAY = new IntegerProperty(prefKey("initial-move-delay"), 200).cached();
100 private static final CachingProperty<Double> SNAP_DISTANCE_METRIC = new DoubleProperty(prefKey("snap-distance-metric"), 0.5).cached();
101 private static final CachingProperty<Double> SNAP_DISTANCE_IMPERIAL = new DoubleProperty(prefKey("snap-distance-imperial"), 1).cached();
102 private static final CachingProperty<Double> SNAP_DISTANCE_CHINESE = new DoubleProperty(prefKey("snap-distance-chinese"), 1).cached();
103 private static final CachingProperty<Double> SNAP_DISTANCE_NAUTICAL = new DoubleProperty(prefKey("snap-distance-nautical"), 0.1).cached();
104 private static final CachingProperty<Color> MAIN_COLOR = new NamedColorProperty(marktr("make parallel helper line"), Color.RED).cached();
105
106 private static final CachingProperty<Map<Modifier, Boolean>> SNAP_MODIFIER_COMBO
107 = new KeyboardModifiersProperty(prefKey("snap-modifier-combo"), "?sC").cached();
108 private static final CachingProperty<Map<Modifier, Boolean>> COPY_TAGS_MODIFIER_COMBO
109 = new KeyboardModifiersProperty(prefKey("copy-tags-modifier-combo"), "As?").cached();
110 private static final CachingProperty<Map<Modifier, Boolean>> ADD_TO_SELECTION_MODIFIER_COMBO
111 = new KeyboardModifiersProperty(prefKey("add-to-selection-modifier-combo"), "aSc").cached();
112 private static final CachingProperty<Map<Modifier, Boolean>> TOGGLE_SELECTED_MODIFIER_COMBO
113 = new KeyboardModifiersProperty(prefKey("toggle-selection-modifier-combo"), "asC").cached();
114 private static final CachingProperty<Map<Modifier, Boolean>> SET_SELECTED_MODIFIER_COMBO
115 = new KeyboardModifiersProperty(prefKey("set-selection-modifier-combo"), "asc").cached();
116 // CHECKSTYLE.ON: SingleSpaceSeparator
117 // @formatter:on
118
119 enum Mode {
120 DRAGGING, NORMAL
121 }
122
123 //// Preferences and flags
124 // See updateModeLocalPreferences for defaults
125 private Mode mode;
126 private boolean copyTags;
127
128 private boolean snap;
129
130 private final MapView mv;
131
132 // Mouse tracking state
133 private Point mousePressedPos;
134 private boolean mouseIsDown;
135 private long mousePressedTime;
136 private boolean mouseHasBeenDragged;
137
138 private transient WaySegment referenceSegment;
139 private transient ParallelWays pWays;
140 private transient Set<Way> sourceWays;
141 private EastNorth helperLineStart;
142 private EastNorth helperLineEnd;
143
144 private final ParallelWayLayer temporaryLayer = new ParallelWayLayer();
145
146 /**
147 * Constructs a new {@code ParallelWayAction}.
148 * @param mapFrame Map frame
149 */
150 public ParallelWayAction(MapFrame mapFrame) {
151 super(tr("Parallel"), "parallel", tr("Make parallel copies of ways"),
152 Shortcut.registerShortcut("mapmode:parallel", tr("Mode: {0}",
153 tr("Parallel")), KeyEvent.VK_P, Shortcut.SHIFT),
154 ImageProvider.getCursor("normal", "parallel"));
155 setHelpId(ht("/Action/Parallel"));
156 mv = mapFrame.mapView;
157 }
158
159 @Override
160 public void enterMode() {
161 // super.enterMode() updates the status line and cursor so we need our state to be set correctly
162 setMode(Mode.NORMAL);
163 pWays = null;
164 super.enterMode();
165
166 // #19887: overwrite default: we want to show the distance to the original way
167 MainApplication.getMap().statusLine.setAutoLength(false);
168
169 mv.addMouseListener(this);
170 mv.addMouseMotionListener(this);
171 mv.addTemporaryLayer(temporaryLayer);
172
173 // Needed to update the mouse cursor if modifiers are changed when the mouse is motionless
174 MainApplication.getMap().keyDetector.addModifierExListener(this);
175 sourceWays = new LinkedHashSet<>(getLayerManager().getEditDataSet().getSelectedWays());
176 for (Way w : sourceWays) {
177 w.setHighlighted(true);
178 }
179 }
180
181 @Override
182 public void exitMode() {
183 super.exitMode();
184 mv.removeMouseListener(this);
185 mv.removeMouseMotionListener(this);
186 mv.removeTemporaryLayer(temporaryLayer);
187 MapFrame map = MainApplication.getMap();
188 map.statusLine.setDist(-1);
189 map.keyDetector.removeModifierExListener(this);
190 removeWayHighlighting(sourceWays);
191 pWays = null;
192 sourceWays = null;
193 referenceSegment = null;
194 }
195
196 @Override
197 public String getModeHelpText() {
198 // TODO: add more detailed feedback based on modifier state.
199 // TODO: dynamic messages based on preferences. (Could be problematic translation wise)
200 if (mode == Mode.NORMAL) {
201 // CHECKSTYLE.OFF: LineLength
202 return tr("Select ways as in Select mode. Drag selected ways or a single way to create a parallel copy (Alt toggles tag preservation)");
203 // CHECKSTYLE.ON: LineLength
204 } else { // mode == DRAGGING
205 return tr("Hold Ctrl to toggle snapping");
206 }
207 }
208
209 @Override
210 public boolean layerIsSupported(Layer l) {
211 return isEditableDataLayer(l);
212 }
213
214 @Override
215 public void modifiersExChanged(int modifiers) {
216 if (MainApplication.getMap() == null || mv == null || !mv.isActiveLayerDrawable())
217 return;
218
219 // Should only get InputEvents due to the mask in enterMode
220 if (updateModifiersState(modifiers)) {
221 updateStatusLine();
222 updateCursor();
223 }
224 }
225
226 private boolean updateModifiersState(int modifiers) {
227 boolean oldAlt = alt;
228 boolean oldShift = shift;
229 boolean oldCtrl = ctrl;
230 updateKeyModifiersEx(modifiers);
231 return oldAlt != alt || oldShift != shift || oldCtrl != ctrl;
232 }
233
234 private void updateCursor() {
235 Cursor newCursor = null;
236 switch (mode) {
237 case NORMAL:
238 if (matchesCurrentModifiers(SET_SELECTED_MODIFIER_COMBO)) {
239 newCursor = ImageProvider.getCursor("normal", "parallel");
240 } else if (matchesCurrentModifiers(ADD_TO_SELECTION_MODIFIER_COMBO)) {
241 newCursor = ImageProvider.getCursor("normal", "parallel_add");
242 } else if (matchesCurrentModifiers(TOGGLE_SELECTED_MODIFIER_COMBO)) {
243 newCursor = ImageProvider.getCursor("normal", "parallel_remove");
244 }
245 break;
246 case DRAGGING:
247 newCursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
248 break;
249 }
250 if (newCursor != null) {
251 mv.setNewCursor(newCursor, this);
252 }
253 }
254
255 private void setMode(Mode mode) {
256 this.mode = mode;
257 updateCursor();
258 updateStatusLine();
259 }
260
261 private boolean sanityCheck() {
262 // @formatter:off
263 boolean areWeSane =
264 mv.isActiveLayerVisible() &&
265 mv.isActiveLayerDrawable() &&
266 ((Boolean) this.getValue("active"));
267 // @formatter:on
268 assert areWeSane; // mad == bad
269 return areWeSane;
270 }
271
272 @Override
273 public void mousePressed(MouseEvent e) {
274 requestFocusInMapView();
275 updateModifiersState(e.getModifiersEx());
276 // Other buttons are off limit, but we still get events.
277 if (e.getButton() != MouseEvent.BUTTON1)
278 return;
279
280 if (!sanityCheck())
281 return;
282
283 updateFlagsOnlyChangeableOnPress();
284 updateFlagsChangeableAlways();
285
286 // Since the created way is left selected, we need to unselect again here
287 if (pWays != null && pWays.getWays() != null) {
288 final List<Way> ways = new ArrayList<>(pWays.getWays());
289 ways.removeIf(w -> w.getDataSet() == null);
290 getLayerManager().getEditDataSet().clearSelection(ways);
291 pWays = null;
292 }
293
294 mouseIsDown = true;
295 mousePressedPos = e.getPoint();
296 mousePressedTime = System.currentTimeMillis();
297
298 }
299
300 @Override
301 public void mouseReleased(MouseEvent e) {
302 updateModifiersState(e.getModifiersEx());
303 // Other buttons are off limit, but we still get events.
304 if (e.getButton() != MouseEvent.BUTTON1)
305 return;
306
307 if (!mouseHasBeenDragged) {
308 // use point from press or click event? (or are these always the same)
309 Way nearestWay = mv.getNearestWay(e.getPoint(), OsmPrimitive::isSelectable);
310 if (nearestWay == null) {
311 if (matchesCurrentModifiers(SET_SELECTED_MODIFIER_COMBO)) {
312 clearSourceWays();
313 }
314 resetMouseTrackingState();
315 return;
316 }
317 boolean isSelected = nearestWay.isSelected();
318 if (matchesCurrentModifiers(ADD_TO_SELECTION_MODIFIER_COMBO)) {
319 if (!isSelected) {
320 addSourceWay(nearestWay);
321 }
322 } else if (matchesCurrentModifiers(TOGGLE_SELECTED_MODIFIER_COMBO)) {
323 if (isSelected) {
324 removeSourceWay(nearestWay);
325 } else {
326 addSourceWay(nearestWay);
327 }
328 } else if (matchesCurrentModifiers(SET_SELECTED_MODIFIER_COMBO)) {
329 clearSourceWays();
330 addSourceWay(nearestWay);
331 } // else -> invalid modifier combination
332 } else if (mode == Mode.DRAGGING) {
333 clearSourceWays();
334 MainApplication.getMap().statusLine.setDist(pWays.getWays());
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(
394 (ILatLon) 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());
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() != matchesCurrentModifiers(COPY_TAGS_MODIFIER_COMBO);
466 }
467
468 private void updateFlagsChangeableAlways() {
469 snap = SNAP_DEFAULT.get() != 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 this(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 (int i = 0; i < string.length(); i++) {
585 char c = string.charAt(i);
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 final 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.