001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions.mapmode; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.marktr; 006import static org.openstreetmap.josm.tools.I18n.tr; 007 008import java.awt.BasicStroke; 009import java.awt.Color; 010import java.awt.Cursor; 011import java.awt.Graphics2D; 012import java.awt.Point; 013import java.awt.event.KeyEvent; 014import java.awt.event.MouseEvent; 015import java.util.Collection; 016import java.util.Collections; 017import java.util.EnumMap; 018import java.util.EnumSet; 019import java.util.LinkedHashSet; 020import java.util.Map; 021import java.util.Optional; 022import java.util.Set; 023import java.util.stream.Stream; 024 025import javax.swing.JOptionPane; 026 027import org.openstreetmap.josm.data.Bounds; 028import org.openstreetmap.josm.data.SystemOfMeasurement; 029import org.openstreetmap.josm.data.coor.EastNorth; 030import org.openstreetmap.josm.data.osm.Node; 031import org.openstreetmap.josm.data.osm.OsmPrimitive; 032import org.openstreetmap.josm.data.osm.Way; 033import org.openstreetmap.josm.data.osm.WaySegment; 034import org.openstreetmap.josm.data.preferences.AbstractToStringProperty; 035import org.openstreetmap.josm.data.preferences.BooleanProperty; 036import org.openstreetmap.josm.data.preferences.CachingProperty; 037import org.openstreetmap.josm.data.preferences.DoubleProperty; 038import org.openstreetmap.josm.data.preferences.IntegerProperty; 039import org.openstreetmap.josm.data.preferences.NamedColorProperty; 040import org.openstreetmap.josm.data.preferences.StrokeProperty; 041import org.openstreetmap.josm.gui.MainApplication; 042import org.openstreetmap.josm.gui.MapFrame; 043import org.openstreetmap.josm.gui.MapView; 044import org.openstreetmap.josm.gui.Notification; 045import org.openstreetmap.josm.gui.draw.MapViewPath; 046import org.openstreetmap.josm.gui.layer.AbstractMapViewPaintable; 047import org.openstreetmap.josm.gui.layer.Layer; 048import org.openstreetmap.josm.gui.util.ModifierExListener; 049import org.openstreetmap.josm.tools.CheckParameterUtil; 050import org.openstreetmap.josm.tools.Geometry; 051import org.openstreetmap.josm.tools.ImageProvider; 052import org.openstreetmap.josm.tools.Logging; 053import org.openstreetmap.josm.tools.Shortcut; 054 055/** 056 * MapMode for making parallel ways. 057 * 058 * All calculations are done in projected coordinates. 059 * 060 * TODO: 061 * == Functionality == 062 * 063 * 1. Use selected nodes as split points for the selected ways. 064 * 065 * The ways containing the selected nodes will be split and only the "inner" 066 * parts will be copied 067 * 068 * 2. Enter exact offset 069 * 070 * 3. Improve snapping 071 * 072 * 4. Visual cues could be better 073 * 074 * 5. (long term) Parallelize and adjust offsets of existing ways 075 * 076 * == Code quality == 077 * 078 * a) The mode, flags, and modifiers might be updated more than necessary. 079 * 080 * Not a performance problem, but better if they where more centralized 081 * 082 * b) Extract generic MapMode services into a super class and/or utility class 083 * 084 * c) Maybe better to simply draw our own source way highlighting? 085 * 086 * Current code doesn't not take into account that ways might been highlighted 087 * by other than us. Don't think that situation should ever happen though. 088 * 089 * @author Ole Jørgen Brønner (olejorgenb) 090 */ 091public class ParallelWayAction extends MapMode implements ModifierExListener { 092 093 private static final CachingProperty<BasicStroke> HELPER_LINE_STROKE = new StrokeProperty(prefKey("stroke.hepler-line"), "1").cached(); 094 private static final CachingProperty<BasicStroke> REF_LINE_STROKE = new StrokeProperty(prefKey("stroke.ref-line"), "2 2 3").cached(); 095 096 // @formatter:off 097 // CHECKSTYLE.OFF: SingleSpaceSeparator 098 private static final CachingProperty<Double> SNAP_THRESHOLD = new DoubleProperty(prefKey("snap-threshold-percent"), 0.70).cached(); 099 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 (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 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}