source: josm/trunk/src/org/openstreetmap/josm/actions/mapmode/ExtrudeAction.java@ 15586

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

code cleanup

  • Property svn:eol-style set to native
File size: 51.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.Rectangle;
14import java.awt.Stroke;
15import java.awt.event.ActionEvent;
16import java.awt.event.KeyEvent;
17import java.awt.event.MouseEvent;
18import java.awt.geom.AffineTransform;
19import java.awt.geom.GeneralPath;
20import java.awt.geom.Line2D;
21import java.awt.geom.NoninvertibleTransformException;
22import java.awt.geom.Point2D;
23import java.util.ArrayList;
24import java.util.Collection;
25import java.util.LinkedList;
26import java.util.List;
27
28import javax.swing.JCheckBoxMenuItem;
29import javax.swing.JMenuItem;
30
31import org.openstreetmap.josm.actions.JosmAction;
32import org.openstreetmap.josm.actions.MergeNodesAction;
33import org.openstreetmap.josm.command.AddCommand;
34import org.openstreetmap.josm.command.ChangeCommand;
35import org.openstreetmap.josm.command.Command;
36import org.openstreetmap.josm.command.MoveCommand;
37import org.openstreetmap.josm.command.SequenceCommand;
38import org.openstreetmap.josm.data.Bounds;
39import org.openstreetmap.josm.data.UndoRedoHandler;
40import org.openstreetmap.josm.data.coor.EastNorth;
41import org.openstreetmap.josm.data.osm.DataIntegrityProblemException;
42import org.openstreetmap.josm.data.osm.DataSet;
43import org.openstreetmap.josm.data.osm.Node;
44import org.openstreetmap.josm.data.osm.OsmPrimitive;
45import org.openstreetmap.josm.data.osm.Way;
46import org.openstreetmap.josm.data.osm.WaySegment;
47import org.openstreetmap.josm.data.preferences.NamedColorProperty;
48import org.openstreetmap.josm.data.projection.ProjectionRegistry;
49import org.openstreetmap.josm.gui.MainApplication;
50import org.openstreetmap.josm.gui.MainMenu;
51import org.openstreetmap.josm.gui.MapFrame;
52import org.openstreetmap.josm.gui.MapView;
53import org.openstreetmap.josm.gui.draw.MapViewPath;
54import org.openstreetmap.josm.gui.draw.SymbolShape;
55import org.openstreetmap.josm.gui.layer.Layer;
56import org.openstreetmap.josm.gui.layer.MapViewPaintable;
57import org.openstreetmap.josm.gui.util.GuiHelper;
58import org.openstreetmap.josm.gui.util.KeyPressReleaseListener;
59import org.openstreetmap.josm.gui.util.ModifierExListener;
60import org.openstreetmap.josm.spi.preferences.Config;
61import org.openstreetmap.josm.tools.Geometry;
62import org.openstreetmap.josm.tools.ImageProvider;
63import org.openstreetmap.josm.tools.Logging;
64import org.openstreetmap.josm.tools.Shortcut;
65
66/**
67 * Makes a rectangle from a line, or modifies a rectangle.
68 */
69public class ExtrudeAction extends MapMode implements MapViewPaintable, KeyPressReleaseListener, ModifierExListener {
70
71 enum Mode { extrude, translate, select, create_new, translate_node }
72
73 private Mode mode = Mode.select;
74
75 /**
76 * If {@code true}, when extruding create new node(s) even if segments are parallel.
77 */
78 private boolean alwaysCreateNodes;
79 private boolean nodeDragWithoutCtrl;
80
81 private long mouseDownTime;
82 private transient WaySegment selectedSegment;
83 private transient Node selectedNode;
84 private Color mainColor;
85 private transient Stroke mainStroke;
86
87 /** settings value whether shared nodes should be ignored or not */
88 private boolean ignoreSharedNodes;
89
90 private boolean keepSegmentDirection;
91
92 /**
93 * drawing settings for helper lines
94 */
95 private Color helperColor;
96 private transient Stroke helperStrokeDash;
97 private transient Stroke helperStrokeRA;
98
99 private transient Stroke oldLineStroke;
100 private double symbolSize;
101 /**
102 * Possible directions to move to.
103 */
104 private transient List<ReferenceSegment> possibleMoveDirections;
105
106
107 /**
108 * Collection of nodes that is moved
109 */
110 private transient List<Node> movingNodeList;
111
112 /**
113 * The direction that is currently active.
114 */
115 private transient ReferenceSegment activeMoveDirection;
116
117 /**
118 * The position of the mouse cursor when the drag action was initiated.
119 */
120 private Point initialMousePos;
121 /**
122 * The time which needs to pass between click and release before something
123 * counts as a move, in milliseconds
124 */
125 private int initialMoveDelay = 200;
126 /**
127 * The minimal shift of mouse (in pixels) befire something counts as move
128 */
129 private int initialMoveThreshold = 1;
130
131 /**
132 * The initial EastNorths of node1 and node2
133 */
134 private EastNorth initialN1en;
135 private EastNorth initialN2en;
136 /**
137 * The new EastNorths of node1 and node2
138 */
139 private EastNorth newN1en;
140 private EastNorth newN2en;
141
142 /**
143 * the command that performed last move.
144 */
145 private transient MoveCommand moveCommand;
146 /**
147 * The command used for dual alignment movement.
148 * Needs to be separate, due to two nodes moving in different directions.
149 */
150 private transient MoveCommand moveCommand2;
151
152 /** The cursor for the 'create_new' mode. */
153 private final Cursor cursorCreateNew;
154
155 /** The cursor for the 'translate' mode. */
156 private final Cursor cursorTranslate;
157
158 /** The cursor for the 'alwaysCreateNodes' submode. */
159 private final Cursor cursorCreateNodes;
160
161 private static class ReferenceSegment {
162 public final EastNorth en;
163 public final EastNorth p1;
164 public final EastNorth p2;
165 public final boolean perpendicular;
166
167 ReferenceSegment(EastNorth en, EastNorth p1, EastNorth p2, boolean perpendicular) {
168 this.en = en;
169 this.p1 = p1;
170 this.p2 = p2;
171 this.perpendicular = perpendicular;
172 }
173
174 @Override
175 public String toString() {
176 return "ReferenceSegment[en=" + en + ", p1=" + p1 + ", p2=" + p2 + ", perp=" + perpendicular + ']';
177 }
178 }
179
180 // Dual alignment mode stuff
181 /** {@code true}, if dual alignment mode is enabled. User wants following extrude to be dual aligned. */
182 private boolean dualAlignEnabled;
183 /** {@code true}, if dual alignment is active. User is dragging the mouse, required conditions are met.
184 * Treat {@link #mode} (extrude/translate/create_new) as dual aligned. */
185 private boolean dualAlignActive;
186 /** Dual alignment reference segments */
187 private transient ReferenceSegment dualAlignSegment1, dualAlignSegment2;
188 /** {@code true}, if new segment was collapsed */
189 private boolean dualAlignSegmentCollapsed;
190 // Dual alignment UI stuff
191 private final DualAlignChangeAction dualAlignChangeAction;
192 private final JCheckBoxMenuItem dualAlignCheckboxMenuItem;
193 private final transient Shortcut dualAlignShortcut;
194 private boolean useRepeatedShortcut;
195 private boolean ignoreNextKeyRelease;
196
197 private class DualAlignChangeAction extends JosmAction {
198 DualAlignChangeAction() {
199 super(tr("Dual alignment"), /* ICON() */ "mapmode/extrude/dualalign",
200 tr("Switch dual alignment mode while extruding"), null, false);
201 setHelpId(ht("/Action/Extrude#DualAlign"));
202 }
203
204 @Override
205 public void actionPerformed(ActionEvent e) {
206 toggleDualAlign();
207 }
208
209 @Override
210 protected void updateEnabledState() {
211 MapFrame map = MainApplication.getMap();
212 setEnabled(map != null && map.mapMode instanceof ExtrudeAction);
213 }
214 }
215
216 /**
217 * Creates a new ExtrudeAction
218 * @since 11713
219 */
220 public ExtrudeAction() {
221 super(tr("Extrude"), /* ICON(mapmode/) */ "extrude/extrude", tr("Create areas"),
222 Shortcut.registerShortcut("mapmode:extrude", tr("Mode: {0}", tr("Extrude")), KeyEvent.VK_X, Shortcut.DIRECT),
223 ImageProvider.getCursor("normal", "rectangle"));
224 setHelpId(ht("/Action/Extrude"));
225 cursorCreateNew = ImageProvider.getCursor("normal", "rectangle_plus");
226 cursorTranslate = ImageProvider.getCursor("normal", "rectangle_move");
227 cursorCreateNodes = ImageProvider.getCursor("normal", "rectangle_plussmall");
228
229 dualAlignEnabled = false;
230 dualAlignChangeAction = new DualAlignChangeAction();
231 dualAlignCheckboxMenuItem = addDualAlignMenuItem();
232 dualAlignCheckboxMenuItem.getAction().setEnabled(false);
233 dualAlignCheckboxMenuItem.setState(dualAlignEnabled);
234 dualAlignShortcut = Shortcut.registerShortcut("mapmode:extrudedualalign",
235 tr("Mode: {0}", tr("Extrude Dual alignment")), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE);
236 readPreferences(); // to show prefernces in table before entering the mode
237 }
238
239 @Override
240 public void destroy() {
241 super.destroy();
242 dualAlignChangeAction.destroy();
243 }
244
245 private JCheckBoxMenuItem addDualAlignMenuItem() {
246 int n = MainApplication.getMenu().editMenu.getItemCount();
247 for (int i = n-1; i > 0; i--) {
248 JMenuItem item = MainApplication.getMenu().editMenu.getItem(i);
249 if (item != null && item.getAction() != null && item.getAction() instanceof DualAlignChangeAction) {
250 MainApplication.getMenu().editMenu.remove(i);
251 }
252 }
253 return MainMenu.addWithCheckbox(MainApplication.getMenu().editMenu, dualAlignChangeAction, MainMenu.WINDOW_MENU_GROUP.VOLATILE);
254 }
255
256 // -------------------------------------------------------------------------
257 // Mode methods
258 // -------------------------------------------------------------------------
259
260 @Override
261 public String getModeHelpText() {
262 StringBuilder rv;
263 if (mode == Mode.select) {
264 rv = new StringBuilder(tr("Drag a way segment to make a rectangle. Ctrl-drag to move a segment along its normal, " +
265 "Alt-drag to create a new rectangle, double click to add a new node."));
266 if (dualAlignEnabled) {
267 rv.append(' ').append(tr("Dual alignment active."));
268 if (dualAlignSegmentCollapsed)
269 rv.append(' ').append(tr("Segment collapsed due to its direction reversing."));
270 }
271 } else {
272 if (mode == Mode.translate)
273 rv = new StringBuilder(tr("Move a segment along its normal, then release the mouse button."));
274 else if (mode == Mode.translate_node)
275 rv = new StringBuilder(tr("Move the node along one of the segments, then release the mouse button."));
276 else if (mode == Mode.extrude || mode == Mode.create_new)
277 rv = new StringBuilder(tr("Draw a rectangle of the desired size, then release the mouse button."));
278 else {
279 Logging.warn("Extrude: unknown mode " + mode);
280 rv = new StringBuilder();
281 }
282 if (dualAlignActive) {
283 rv.append(' ').append(tr("Dual alignment active."));
284 if (dualAlignSegmentCollapsed) {
285 rv.append(' ').append(tr("Segment collapsed due to its direction reversing."));
286 }
287 }
288 }
289 return rv.toString();
290 }
291
292 @Override
293 public boolean layerIsSupported(Layer l) {
294 return isEditableDataLayer(l);
295 }
296
297 @Override
298 public void enterMode() {
299 super.enterMode();
300 MapFrame map = MainApplication.getMap();
301 map.mapView.addMouseListener(this);
302 map.mapView.addMouseMotionListener(this);
303 ignoreNextKeyRelease = true;
304 map.keyDetector.addKeyListener(this);
305 map.keyDetector.addModifierExListener(this);
306 }
307
308 @Override
309 protected void readPreferences() {
310 initialMoveDelay = Config.getPref().getInt("edit.initial-move-delay", 200);
311 initialMoveThreshold = Config.getPref().getInt("extrude.initial-move-threshold", 1);
312 mainColor = new NamedColorProperty(marktr("Extrude: main line"), Color.RED).get();
313 helperColor = new NamedColorProperty(marktr("Extrude: helper line"), Color.ORANGE).get();
314 helperStrokeDash = GuiHelper.getCustomizedStroke(Config.getPref().get("extrude.stroke.helper-line", "1 4"));
315 helperStrokeRA = new BasicStroke(1);
316 symbolSize = Config.getPref().getDouble("extrude.angle-symbol-radius", 8);
317 nodeDragWithoutCtrl = Config.getPref().getBoolean("extrude.drag-nodes-without-ctrl", false);
318 oldLineStroke = GuiHelper.getCustomizedStroke(Config.getPref().get("extrude.ctrl.stroke.old-line", "1"));
319 mainStroke = GuiHelper.getCustomizedStroke(Config.getPref().get("extrude.stroke.main", "3"));
320
321 ignoreSharedNodes = Config.getPref().getBoolean("extrude.ignore-shared-nodes", true);
322 dualAlignCheckboxMenuItem.getAction().setEnabled(true);
323 useRepeatedShortcut = Config.getPref().getBoolean("extrude.dualalign.toggleOnRepeatedX", true);
324 keepSegmentDirection = Config.getPref().getBoolean("extrude.dualalign.keep-segment-direction", true);
325 }
326
327 @Override
328 public void exitMode() {
329 MapFrame map = MainApplication.getMap();
330 map.mapView.removeMouseListener(this);
331 map.mapView.removeMouseMotionListener(this);
332 map.mapView.removeTemporaryLayer(this);
333 dualAlignCheckboxMenuItem.getAction().setEnabled(false);
334 map.keyDetector.removeKeyListener(this);
335 map.keyDetector.removeModifierExListener(this);
336 super.exitMode();
337 }
338
339 // -------------------------------------------------------------------------
340 // Event handlers
341 // -------------------------------------------------------------------------
342
343 /**
344 * This method is called to indicate different modes via cursor when the Alt/Ctrl/Shift modifier is pressed,
345 */
346 @Override
347 public void modifiersExChanged(int modifiers) {
348 MapFrame map = MainApplication.getMap();
349 if (!MainApplication.isDisplayingMapView() || !map.mapView.isActiveLayerDrawable())
350 return;
351 updateKeyModifiersEx(modifiers);
352 if (mode == Mode.select) {
353 map.mapView.setNewCursor(ctrl ? cursorTranslate : alt ? cursorCreateNew : shift ? cursorCreateNodes : cursor, this);
354 }
355 }
356
357 @Override
358 public void doKeyPressed(KeyEvent e) {
359 // Do nothing
360 }
361
362 @Override
363 public void doKeyReleased(KeyEvent e) {
364 if (!dualAlignShortcut.isEvent(e) && !(useRepeatedShortcut && getShortcut().isEvent(e)))
365 return;
366 if (ignoreNextKeyRelease) {
367 ignoreNextKeyRelease = false;
368 } else {
369 toggleDualAlign();
370 }
371 }
372
373 /**
374 * Toggles dual alignment mode.
375 */
376 private void toggleDualAlign() {
377 dualAlignEnabled = !dualAlignEnabled;
378 dualAlignCheckboxMenuItem.setState(dualAlignEnabled);
379 updateStatusLine();
380 }
381
382 /**
383 * If the left mouse button is pressed over a segment or a node, switches
384 * to appropriate {@link #mode}, depending on Ctrl/Alt/Shift modifiers and
385 * {@link #dualAlignEnabled}.
386 * @param e current mouse event
387 */
388 @Override
389 public void mousePressed(MouseEvent e) {
390 MapFrame map = MainApplication.getMap();
391 if (!map.mapView.isActiveLayerVisible())
392 return;
393 if (!(Boolean) this.getValue("active"))
394 return;
395 if (e.getButton() != MouseEvent.BUTTON1)
396 return;
397
398 requestFocusInMapView();
399 updateKeyModifiers(e);
400
401 selectedNode = map.mapView.getNearestNode(e.getPoint(), OsmPrimitive::isSelectable);
402 selectedSegment = map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive::isSelectable);
403
404 // If nothing gets caught, stay in select mode
405 if (selectedSegment == null && selectedNode == null) return;
406
407 if (selectedNode != null) {
408 if (ctrl || nodeDragWithoutCtrl) {
409 movingNodeList = new ArrayList<>();
410 movingNodeList.add(selectedNode);
411 calculatePossibleDirectionsByNode();
412 if (possibleMoveDirections.isEmpty()) {
413 // if no directions fould, do not enter dragging mode
414 return;
415 }
416 mode = Mode.translate_node;
417 dualAlignActive = false;
418 }
419 } else {
420 // Otherwise switch to another mode
421 if (dualAlignEnabled && checkDualAlignConditions()) {
422 dualAlignActive = true;
423 calculatePossibleDirectionsForDualAlign();
424 dualAlignSegmentCollapsed = false;
425 } else {
426 dualAlignActive = false;
427 calculatePossibleDirectionsBySegment();
428 }
429 if (ctrl) {
430 mode = Mode.translate;
431 movingNodeList = new ArrayList<>();
432 movingNodeList.add(selectedSegment.getFirstNode());
433 movingNodeList.add(selectedSegment.getSecondNode());
434 } else if (alt) {
435 mode = Mode.create_new;
436 // create a new segment and then select and extrude the new segment
437 getLayerManager().getEditDataSet().setSelected(selectedSegment.way);
438 alwaysCreateNodes = true;
439 } else {
440 mode = Mode.extrude;
441 getLayerManager().getEditDataSet().setSelected(selectedSegment.way);
442 alwaysCreateNodes = shift;
443 }
444 }
445
446 // Signifies that nothing has happened yet
447 newN1en = null;
448 newN2en = null;
449 moveCommand = null;
450 moveCommand2 = null;
451
452 map.mapView.addTemporaryLayer(this);
453
454 updateStatusLine();
455 map.mapView.repaint();
456
457 // Make note of time pressed
458 mouseDownTime = System.currentTimeMillis();
459
460 // Make note of mouse position
461 initialMousePos = e.getPoint();
462 }
463
464 /**
465 * Performs action depending on what {@link #mode} we're in.
466 * @param e current mouse event
467 */
468 @Override
469 public void mouseDragged(MouseEvent e) {
470 MapView mapView = MainApplication.getMap().mapView;
471 if (!mapView.isActiveLayerVisible())
472 return;
473
474 // do not count anything as a drag if it lasts less than 100 milliseconds.
475 if (System.currentTimeMillis() - mouseDownTime < initialMoveDelay)
476 return;
477
478 if (mode == Mode.select) {
479 // Just sit tight and wait for mouse to be released.
480 } else {
481 //move, create new and extrude mode - move the selected segment
482
483 EastNorth mouseEn = mapView.getEastNorth(e.getPoint().x, e.getPoint().y);
484 EastNorth bestMovement = calculateBestMovementAndNewNodes(mouseEn);
485
486 mapView.setNewCursor(Cursor.MOVE_CURSOR, this);
487
488 if (dualAlignActive) {
489 if (mode == Mode.extrude || mode == Mode.create_new) {
490 // nothing here
491 } else if (mode == Mode.translate) {
492 EastNorth movement1 = newN1en.subtract(initialN1en);
493 EastNorth movement2 = newN2en.subtract(initialN2en);
494 // move nodes to new position
495 if (moveCommand == null || moveCommand2 == null) {
496 // make a new move commands
497 moveCommand = new MoveCommand(movingNodeList.get(0), movement1.getX(), movement1.getY());
498 moveCommand2 = new MoveCommand(movingNodeList.get(1), movement2.getX(), movement2.getY());
499 Command c = new SequenceCommand(tr("Extrude Way"), moveCommand, moveCommand2);
500 UndoRedoHandler.getInstance().add(c);
501 } else {
502 // reuse existing move commands
503 moveCommand.moveAgainTo(movement1.getX(), movement1.getY());
504 moveCommand2.moveAgainTo(movement2.getX(), movement2.getY());
505 }
506 }
507 } else if (bestMovement != null) {
508 if (mode == Mode.extrude || mode == Mode.create_new) {
509 //nothing here
510 } else if (mode == Mode.translate_node || mode == Mode.translate) {
511 //move nodes to new position
512 if (moveCommand == null) {
513 //make a new move command
514 moveCommand = new MoveCommand(new ArrayList<OsmPrimitive>(movingNodeList), bestMovement);
515 UndoRedoHandler.getInstance().add(moveCommand);
516 } else {
517 //reuse existing move command
518 moveCommand.moveAgainTo(bestMovement.getX(), bestMovement.getY());
519 }
520 }
521 }
522
523 mapView.repaint();
524 }
525 }
526
527 /**
528 * Does anything that needs to be done, then switches back to select mode.
529 * @param e current mouse event
530 */
531 @Override
532 public void mouseReleased(MouseEvent e) {
533
534 MapView mapView = MainApplication.getMap().mapView;
535 if (!mapView.isActiveLayerVisible())
536 return;
537
538 if (mode == Mode.select) {
539 // Nothing to be done
540 } else {
541 if (mode == Mode.create_new) {
542 if (e.getPoint().distance(initialMousePos) > initialMoveThreshold && newN1en != null) {
543 createNewRectangle();
544 }
545 } else if (mode == Mode.extrude) {
546 if (e.getClickCount() == 2 && e.getPoint().equals(initialMousePos)) {
547 // double click adds a new node
548 addNewNode(e);
549 } else if (e.getPoint().distance(initialMousePos) > initialMoveThreshold && newN1en != null && selectedSegment != null) {
550 try {
551 // main extrusion commands
552 performExtrusion();
553 } catch (DataIntegrityProblemException ex) {
554 // Can occur if calling undo while extruding, see #12870
555 Logging.error(ex);
556 }
557 }
558 } else if (mode == Mode.translate || mode == Mode.translate_node) {
559 //Commit translate
560 //the move command is already committed in mouseDragged
561 joinNodesIfCollapsed(movingNodeList);
562 }
563
564 updateKeyModifiers(e);
565 // Switch back into select mode
566 mapView.setNewCursor(ctrl ? cursorTranslate : alt ? cursorCreateNew : shift ? cursorCreateNodes : cursor, this);
567 mapView.removeTemporaryLayer(this);
568 selectedSegment = null;
569 moveCommand = null;
570 mode = Mode.select;
571 dualAlignSegmentCollapsed = false;
572 updateStatusLine();
573 mapView.repaint();
574 }
575 }
576
577 // -------------------------------------------------------------------------
578 // Custom methods
579 // -------------------------------------------------------------------------
580
581 /**
582 * Inserts node into nearby segment.
583 * @param e current mouse point
584 */
585 private static void addNewNode(MouseEvent e) {
586 // Should maybe do the same as in DrawAction and fetch all nearby segments?
587 MapView mapView = MainApplication.getMap().mapView;
588 WaySegment ws = mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive::isSelectable);
589 if (ws != null) {
590 Node n = new Node(mapView.getLatLon(e.getX(), e.getY()));
591 EastNorth a = ws.getFirstNode().getEastNorth();
592 EastNorth b = ws.getSecondNode().getEastNorth();
593 n.setEastNorth(Geometry.closestPointToSegment(a, b, n.getEastNorth()));
594 Way wnew = new Way(ws.way);
595 wnew.addNode(ws.lowerIndex+1, n);
596 DataSet ds = ws.way.getDataSet();
597 UndoRedoHandler.getInstance().add(new SequenceCommand(tr("Add a new node to an existing way"),
598 new AddCommand(ds, n), new ChangeCommand(ds, ws.way, wnew)));
599 }
600 }
601
602 /**
603 * Creates a new way that shares segment with selected way.
604 */
605 private void createNewRectangle() {
606 if (selectedSegment == null) return;
607 DataSet ds = getLayerManager().getEditDataSet();
608 // create a new rectangle
609 Collection<Command> cmds = new LinkedList<>();
610 Node third = new Node(newN2en);
611 Node fourth = new Node(newN1en);
612 Way wnew = new Way();
613 wnew.addNode(selectedSegment.getFirstNode());
614 wnew.addNode(selectedSegment.getSecondNode());
615 wnew.addNode(third);
616 if (!dualAlignSegmentCollapsed) {
617 // rectangle can degrade to triangle for dual alignment after collapsing
618 wnew.addNode(fourth);
619 }
620 // ... and close the way
621 wnew.addNode(selectedSegment.getFirstNode());
622 // undo support
623 cmds.add(new AddCommand(ds, third));
624 if (!dualAlignSegmentCollapsed) {
625 cmds.add(new AddCommand(ds, fourth));
626 }
627 cmds.add(new AddCommand(ds, wnew));
628 Command c = new SequenceCommand(tr("Extrude Way"), cmds);
629 UndoRedoHandler.getInstance().add(c);
630 ds.setSelected(wnew);
631 }
632
633 /**
634 * Does actual extrusion of {@link #selectedSegment}.
635 * Uses {@link #initialN1en}, {@link #initialN2en} saved in calculatePossibleDirections* call
636 * Uses {@link #newN1en}, {@link #newN2en} calculated by {@link #calculateBestMovementAndNewNodes}
637 */
638 private void performExtrusion() {
639 DataSet ds = getLayerManager().getEditDataSet();
640 // create extrusion
641 Collection<Command> cmds = new LinkedList<>();
642 Way wnew = new Way(selectedSegment.way);
643 boolean wayWasModified = false;
644 boolean wayWasSingleSegment = wnew.getNodesCount() == 2;
645 int insertionPoint = selectedSegment.lowerIndex + 1;
646
647 //find if the new points overlap existing segments (in case of 90 degree angles)
648 Node prevNode = getPreviousNode(selectedSegment.lowerIndex);
649 boolean nodeOverlapsSegment = prevNode != null && Geometry.segmentsParallel(initialN1en, prevNode.getEastNorth(), initialN1en, newN1en);
650 // segmentAngleZero marks subset of nodeOverlapsSegment.
651 // nodeOverlapsSegment is true if angle between segments is 0 or PI, segmentAngleZero only if angle is 0
652 boolean segmentAngleZero = prevNode != null && Math.abs(Geometry.getCornerAngle(prevNode.getEastNorth(), initialN1en, newN1en)) < 1e-5;
653 boolean hasOtherWays = hasNodeOtherWays(selectedSegment.getFirstNode(), selectedSegment.way);
654 List<Node> changedNodes = new ArrayList<>();
655 if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) {
656 //move existing node
657 Node n1Old = selectedSegment.getFirstNode();
658 cmds.add(new MoveCommand(n1Old, ProjectionRegistry.getProjection().eastNorth2latlon(newN1en)));
659 changedNodes.add(n1Old);
660 } else if (ignoreSharedNodes && segmentAngleZero && !alwaysCreateNodes && hasOtherWays) {
661 // replace shared node with new one
662 Node n1Old = selectedSegment.getFirstNode();
663 Node n1New = new Node(ProjectionRegistry.getProjection().eastNorth2latlon(newN1en));
664 wnew.addNode(insertionPoint, n1New);
665 wnew.removeNode(n1Old);
666 wayWasModified = true;
667 cmds.add(new AddCommand(ds, n1New));
668 changedNodes.add(n1New);
669 } else {
670 //introduce new node
671 Node n1New = new Node(ProjectionRegistry.getProjection().eastNorth2latlon(newN1en));
672 wnew.addNode(insertionPoint, n1New);
673 wayWasModified = true;
674 insertionPoint++;
675 cmds.add(new AddCommand(ds, n1New));
676 changedNodes.add(n1New);
677 }
678
679 //find if the new points overlap existing segments (in case of 90 degree angles)
680 Node nextNode = getNextNode(selectedSegment.lowerIndex + 1);
681 nodeOverlapsSegment = nextNode != null && Geometry.segmentsParallel(initialN2en, nextNode.getEastNorth(), initialN2en, newN2en);
682 segmentAngleZero = nextNode != null && Math.abs(Geometry.getCornerAngle(nextNode.getEastNorth(), initialN2en, newN2en)) < 1e-5;
683 hasOtherWays = hasNodeOtherWays(selectedSegment.getSecondNode(), selectedSegment.way);
684
685 if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) {
686 //move existing node
687 Node n2Old = selectedSegment.getSecondNode();
688 cmds.add(new MoveCommand(n2Old, ProjectionRegistry.getProjection().eastNorth2latlon(newN2en)));
689 changedNodes.add(n2Old);
690 } else if (ignoreSharedNodes && segmentAngleZero && !alwaysCreateNodes && hasOtherWays) {
691 // replace shared node with new one
692 Node n2Old = selectedSegment.getSecondNode();
693 Node n2New = new Node(ProjectionRegistry.getProjection().eastNorth2latlon(newN2en));
694 wnew.addNode(insertionPoint, n2New);
695 wnew.removeNode(n2Old);
696 wayWasModified = true;
697 cmds.add(new AddCommand(ds, n2New));
698 changedNodes.add(n2New);
699 } else {
700 //introduce new node
701 Node n2New = new Node(ProjectionRegistry.getProjection().eastNorth2latlon(newN2en));
702 wnew.addNode(insertionPoint, n2New);
703 wayWasModified = true;
704 cmds.add(new AddCommand(ds, n2New));
705 changedNodes.add(n2New);
706 }
707
708 //the way was a single segment, close the way
709 if (wayWasSingleSegment) {
710 wnew.addNode(selectedSegment.getFirstNode());
711 wayWasModified = true;
712 }
713 if (wayWasModified) {
714 // we only need to change the way if its node list was really modified
715 cmds.add(new ChangeCommand(selectedSegment.way, wnew));
716 }
717 Command c = new SequenceCommand(tr("Extrude Way"), cmds);
718 UndoRedoHandler.getInstance().add(c);
719 joinNodesIfCollapsed(changedNodes);
720 }
721
722 private void joinNodesIfCollapsed(List<Node> changedNodes) {
723 if (!dualAlignActive || newN1en == null || newN2en == null) return;
724 if (newN1en.distance(newN2en) > 1e-6) return;
725 // If the dual alignment moved two nodes to the same point, merge them
726 Node targetNode = MergeNodesAction.selectTargetNode(changedNodes);
727 Node locNode = MergeNodesAction.selectTargetLocationNode(changedNodes);
728 Command mergeCmd = MergeNodesAction.mergeNodes(changedNodes, targetNode, locNode);
729 if (mergeCmd != null) {
730 UndoRedoHandler.getInstance().add(mergeCmd);
731 } else {
732 // undo extruding command itself
733 UndoRedoHandler.getInstance().undo();
734 }
735 }
736
737 /**
738 * This method tests if {@code node} has other ways apart from the given one.
739 * @param node node to test
740 * @param myWay way known to contain this node
741 * @return {@code true} if {@code node} belongs only to {@code myWay}, false if there are more ways.
742 */
743 private static boolean hasNodeOtherWays(Node node, Way myWay) {
744 for (OsmPrimitive p : node.getReferrers()) {
745 if (p instanceof Way && p.isUsable() && p != myWay)
746 return true;
747 }
748 return false;
749 }
750
751 /**
752 * Determines best movement from {@link #initialMousePos} to current mouse position,
753 * choosing one of the directions from {@link #possibleMoveDirections}.
754 * @param mouseEn current mouse position
755 * @return movement vector
756 */
757 private EastNorth calculateBestMovement(EastNorth mouseEn) {
758
759 EastNorth initialMouseEn = MainApplication.getMap().mapView.getEastNorth(initialMousePos.x, initialMousePos.y);
760 EastNorth mouseMovement = mouseEn.subtract(initialMouseEn);
761
762 double bestDistance = Double.POSITIVE_INFINITY;
763 EastNorth bestMovement = null;
764 activeMoveDirection = null;
765
766 //find the best movement direction and vector
767 for (ReferenceSegment direction : possibleMoveDirections) {
768 EastNorth movement = calculateSegmentOffset(initialN1en, initialN2en, direction.en, mouseEn);
769 if (movement == null) {
770 //if direction parallel to segment.
771 continue;
772 }
773
774 double distanceFromMouseMovement = movement.distance(mouseMovement);
775 if (bestDistance > distanceFromMouseMovement) {
776 bestDistance = distanceFromMouseMovement;
777 activeMoveDirection = direction;
778 bestMovement = movement;
779 }
780 }
781 return bestMovement;
782 }
783
784 /***
785 * This method calculates offset amount by which to move the given segment
786 * perpendicularly for it to be in line with mouse position.
787 * @param segmentP1 segment's first point
788 * @param segmentP2 segment's second point
789 * @param moveDirection direction of movement
790 * @param targetPos mouse position
791 * @return offset amount of P1 and P2.
792 */
793 private static EastNorth calculateSegmentOffset(EastNorth segmentP1, EastNorth segmentP2, EastNorth moveDirection,
794 EastNorth targetPos) {
795 EastNorth intersectionPoint;
796 if (segmentP1.distanceSq(segmentP2) > 1e-7) {
797 intersectionPoint = Geometry.getLineLineIntersection(segmentP1, segmentP2, targetPos, targetPos.add(moveDirection));
798 } else {
799 intersectionPoint = Geometry.closestPointToLine(targetPos, targetPos.add(moveDirection), segmentP1);
800 }
801
802 if (intersectionPoint == null)
803 return null;
804 else
805 //return distance form base to target position
806 return targetPos.subtract(intersectionPoint);
807 }
808
809 /**
810 * Gathers possible move directions - perpendicular to the selected segment
811 * and parallel to neighboring segments.
812 */
813 private void calculatePossibleDirectionsBySegment() {
814 // remember initial positions for segment nodes.
815 initialN1en = selectedSegment.getFirstNode().getEastNorth();
816 initialN2en = selectedSegment.getSecondNode().getEastNorth();
817
818 //add direction perpendicular to the selected segment
819 possibleMoveDirections = new ArrayList<>();
820 possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
821 initialN1en.getY() - initialN2en.getY(),
822 initialN2en.getX() - initialN1en.getX()
823 ), initialN1en, initialN2en, true));
824
825
826 //add directions parallel to neighbor segments
827 Node prevNode = getPreviousNode(selectedSegment.lowerIndex);
828 if (prevNode != null) {
829 EastNorth en = prevNode.getEastNorth();
830 possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
831 initialN1en.getX() - en.getX(),
832 initialN1en.getY() - en.getY()
833 ), initialN1en, en, false));
834 }
835
836 Node nextNode = getNextNode(selectedSegment.lowerIndex + 1);
837 if (nextNode != null) {
838 EastNorth en = nextNode.getEastNorth();
839 possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
840 initialN2en.getX() - en.getX(),
841 initialN2en.getY() - en.getY()
842 ), initialN2en, en, false));
843 }
844 }
845
846 /**
847 * Gathers possible move directions - along all adjacent segments.
848 */
849 private void calculatePossibleDirectionsByNode() {
850 // remember initial positions for segment nodes.
851 initialN1en = selectedNode.getEastNorth();
852 initialN2en = initialN1en;
853 possibleMoveDirections = new ArrayList<>();
854 for (OsmPrimitive p: selectedNode.getReferrers()) {
855 if (p instanceof Way && p.isUsable()) {
856 for (Node neighbor: ((Way) p).getNeighbours(selectedNode)) {
857 EastNorth en = neighbor.getEastNorth();
858 possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
859 initialN1en.getX() - en.getX(),
860 initialN1en.getY() - en.getY()
861 ), initialN1en, en, false));
862 }
863 }
864 }
865 }
866
867 /**
868 * Checks dual alignment conditions:
869 * 1. selected segment has both neighboring segments,
870 * 2. selected segment is not parallel with neighboring segments.
871 * @return {@code true} if dual alignment conditions are satisfied
872 */
873 private boolean checkDualAlignConditions() {
874 Node prevNode = getPreviousNode(selectedSegment.lowerIndex);
875 Node nextNode = getNextNode(selectedSegment.lowerIndex + 1);
876 if (prevNode == null || nextNode == null) {
877 return false;
878 }
879
880 EastNorth n1en = selectedSegment.getFirstNode().getEastNorth();
881 EastNorth n2en = selectedSegment.getSecondNode().getEastNorth();
882 if (n1en.distance(prevNode.getEastNorth()) < 1e-4 ||
883 n2en.distance(nextNode.getEastNorth()) < 1e-4) {
884 return false;
885 }
886
887 boolean prevSegmentParallel = Geometry.segmentsParallel(n1en, prevNode.getEastNorth(), n1en, n2en);
888 boolean nextSegmentParallel = Geometry.segmentsParallel(n2en, nextNode.getEastNorth(), n1en, n2en);
889 return !prevSegmentParallel && !nextSegmentParallel;
890 }
891
892 /**
893 * Gathers possible move directions - perpendicular to the selected segment only.
894 * Neighboring segments go to {@link #dualAlignSegment1} and {@link #dualAlignSegment2}.
895 */
896 private void calculatePossibleDirectionsForDualAlign() {
897 // remember initial positions for segment nodes.
898 initialN1en = selectedSegment.getFirstNode().getEastNorth();
899 initialN2en = selectedSegment.getSecondNode().getEastNorth();
900
901 // add direction perpendicular to the selected segment
902 possibleMoveDirections = new ArrayList<>();
903 possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
904 initialN1en.getY() - initialN2en.getY(),
905 initialN2en.getX() - initialN1en.getX()
906 ), initialN1en, initialN2en, true));
907
908 // set neighboring segments
909 Node prevNode = getPreviousNode(selectedSegment.lowerIndex);
910 if (prevNode != null) {
911 EastNorth prevNodeEn = prevNode.getEastNorth();
912 dualAlignSegment1 = new ReferenceSegment(new EastNorth(
913 initialN1en.getX() - prevNodeEn.getX(),
914 initialN1en.getY() - prevNodeEn.getY()
915 ), initialN1en, prevNodeEn, false);
916 }
917
918 Node nextNode = getNextNode(selectedSegment.lowerIndex + 1);
919 if (nextNode != null) {
920 EastNorth nextNodeEn = nextNode.getEastNorth();
921 dualAlignSegment2 = new ReferenceSegment(new EastNorth(
922 initialN2en.getX() - nextNodeEn.getX(),
923 initialN2en.getY() - nextNodeEn.getY()
924 ), initialN2en, nextNodeEn, false);
925 }
926 }
927
928 /**
929 * Calculate newN1en, newN2en best suitable for given mouse coordinates
930 * For dual align, calculates positions of new nodes, aligning them to neighboring segments.
931 * Elsewhere, just adds the vetor returned by calculateBestMovement to {@link #initialN1en}, {@link #initialN2en}.
932 * @param mouseEn mouse coordinates
933 * @return best movement vector
934 */
935 private EastNorth calculateBestMovementAndNewNodes(EastNorth mouseEn) {
936 EastNorth bestMovement = calculateBestMovement(mouseEn);
937 EastNorth n1movedEn = initialN1en.add(bestMovement), n2movedEn;
938
939 // find out the movement distance, in metres
940 double distance = ProjectionRegistry.getProjection().eastNorth2latlon(initialN1en).greatCircleDistance(
941 ProjectionRegistry.getProjection().eastNorth2latlon(n1movedEn));
942 MainApplication.getMap().statusLine.setDist(distance);
943 updateStatusLine();
944
945 if (dualAlignActive) {
946 // new positions of selected segment's nodes, without applying dual alignment
947 n1movedEn = initialN1en.add(bestMovement);
948 n2movedEn = initialN2en.add(bestMovement);
949
950 // calculate intersections of parallel shifted segment and the adjacent lines
951 newN1en = Geometry.getLineLineIntersection(n1movedEn, n2movedEn, dualAlignSegment1.p1, dualAlignSegment1.p2);
952 newN2en = Geometry.getLineLineIntersection(n1movedEn, n2movedEn, dualAlignSegment2.p1, dualAlignSegment2.p2);
953 if (newN1en == null || newN2en == null) return bestMovement;
954 if (keepSegmentDirection && isOppositeDirection(newN1en, newN2en, initialN1en, initialN2en)) {
955 EastNorth collapsedSegmentPosition = Geometry.getLineLineIntersection(dualAlignSegment1.p1, dualAlignSegment1.p2,
956 dualAlignSegment2.p1, dualAlignSegment2.p2);
957 newN1en = collapsedSegmentPosition;
958 newN2en = collapsedSegmentPosition;
959 dualAlignSegmentCollapsed = true;
960 } else {
961 dualAlignSegmentCollapsed = false;
962 }
963 } else {
964 newN1en = n1movedEn;
965 newN2en = initialN2en.add(bestMovement);
966 }
967 return bestMovement;
968 }
969
970 /**
971 * Gets a node index from selected way before given index.
972 * @param index index of current node
973 * @return index of previous node or <code>-1</code> if there are no nodes there.
974 */
975 private int getPreviousNodeIndex(int index) {
976 if (index > 0)
977 return index - 1;
978 else if (selectedSegment.way.isClosed())
979 return selectedSegment.way.getNodesCount() - 2;
980 else
981 return -1;
982 }
983
984 /**
985 * Gets a node from selected way before given index.
986 * @param index index of current node
987 * @return previous node or <code>null</code> if there are no nodes there.
988 */
989 private Node getPreviousNode(int index) {
990 int indexPrev = getPreviousNodeIndex(index);
991 if (indexPrev >= 0)
992 return selectedSegment.way.getNode(indexPrev);
993 else
994 return null;
995 }
996
997 /**
998 * Gets a node index from selected way after given index.
999 * @param index index of current node
1000 * @return index of next node or <code>-1</code> if there are no nodes there.
1001 */
1002 private int getNextNodeIndex(int index) {
1003 int count = selectedSegment.way.getNodesCount();
1004 if (index < count - 1)
1005 return index + 1;
1006 else if (selectedSegment.way.isClosed())
1007 return 1;
1008 else
1009 return -1;
1010 }
1011
1012 /**
1013 * Gets a node from selected way after given index.
1014 * @param index index of current node
1015 * @return next node or <code>null</code> if there are no nodes there.
1016 */
1017 private Node getNextNode(int index) {
1018 int indexNext = getNextNodeIndex(index);
1019 if (indexNext >= 0)
1020 return selectedSegment.way.getNode(indexNext);
1021 else
1022 return null;
1023 }
1024
1025 // -------------------------------------------------------------------------
1026 // paint methods
1027 // -------------------------------------------------------------------------
1028
1029 @Override
1030 public void paint(Graphics2D g, MapView mv, Bounds box) {
1031 Graphics2D g2 = g;
1032 if (mode == Mode.select) {
1033 // Nothing to do
1034 } else {
1035 if (newN1en != null) {
1036
1037 EastNorth p1 = initialN1en;
1038 EastNorth p2 = initialN2en;
1039 EastNorth p3 = newN1en;
1040 EastNorth p4 = newN2en;
1041
1042 Point2D normalUnitVector = activeMoveDirection != null ? getNormalUniVector() : null;
1043
1044 if (mode == Mode.extrude || mode == Mode.create_new) {
1045 g2.setColor(mainColor);
1046 g2.setStroke(mainStroke);
1047 // Draw rectangle around new area.
1048 MapViewPath b = new MapViewPath(mv);
1049 b.moveTo(p1);
1050 b.lineTo(p3);
1051 b.lineTo(p4);
1052 b.lineTo(p2);
1053 b.lineTo(p1);
1054 g2.draw(b);
1055
1056 if (dualAlignActive) {
1057 // Draw reference ways
1058 drawReferenceSegment(g2, mv, dualAlignSegment1);
1059 drawReferenceSegment(g2, mv, dualAlignSegment2);
1060 } else if (activeMoveDirection != null && normalUnitVector != null) {
1061 // Draw reference way
1062 drawReferenceSegment(g2, mv, activeMoveDirection);
1063
1064 // Draw right angle marker on first node position, only when moving at right angle
1065 if (activeMoveDirection.perpendicular) {
1066 // mirror RightAngle marker, so it is inside the extrude
1067 double headingRefWS = activeMoveDirection.p1.heading(activeMoveDirection.p2);
1068 double headingMoveDir = Math.atan2(normalUnitVector.getY(), normalUnitVector.getX());
1069 double headingDiff = headingRefWS - headingMoveDir;
1070 if (headingDiff < 0)
1071 headingDiff += 2 * Math.PI;
1072 boolean mirrorRA = Math.abs(headingDiff - Math.PI) > 1e-5;
1073 Point pr1 = mv.getPoint(activeMoveDirection.p1);
1074 drawAngleSymbol(g2, pr1, normalUnitVector, mirrorRA);
1075 }
1076 }
1077 } else if (mode == Mode.translate || mode == Mode.translate_node) {
1078 g2.setColor(mainColor);
1079 if (p1.distance(p2) < 3) {
1080 g2.setStroke(mainStroke);
1081 g2.draw(new MapViewPath(mv).shapeAround(p1, SymbolShape.CIRCLE, symbolSize));
1082 } else {
1083 g2.setStroke(oldLineStroke);
1084 g2.draw(new MapViewPath(mv).moveTo(p1).lineTo(p2));
1085 }
1086
1087 if (dualAlignActive) {
1088 // Draw reference ways
1089 drawReferenceSegment(g2, mv, dualAlignSegment1);
1090 drawReferenceSegment(g2, mv, dualAlignSegment2);
1091 } else if (activeMoveDirection != null) {
1092
1093 g2.setColor(helperColor);
1094 g2.setStroke(helperStrokeDash);
1095 // Draw a guideline along the normal.
1096 Point2D centerpoint = mv.getPoint2D(p1.interpolate(p2, .5));
1097 g2.draw(createSemiInfiniteLine(centerpoint, normalUnitVector, g2));
1098 // Draw right angle marker on initial position, only when moving at right angle
1099 if (activeMoveDirection.perpendicular) {
1100 // EastNorth units per pixel
1101 g2.setStroke(helperStrokeRA);
1102 g2.setColor(mainColor);
1103 drawAngleSymbol(g2, centerpoint, normalUnitVector, false);
1104 }
1105 }
1106 }
1107 }
1108 g2.setStroke(helperStrokeRA); // restore default stroke to prevent starnge occasional drawings
1109 }
1110 }
1111
1112 private Point2D getNormalUniVector() {
1113 double fac = 1.0 / activeMoveDirection.en.length();
1114 // mult by factor to get unit vector.
1115 Point2D normalUnitVector = new Point2D.Double(activeMoveDirection.en.getX() * fac, activeMoveDirection.en.getY() * fac);
1116
1117 // Check to see if our new N1 is in a positive direction with respect to the normalUnitVector.
1118 // Even if the x component is zero, we should still be able to discern using +0.0 and -0.0
1119 if (newN1en != null && ((newN1en.getX() > initialN1en.getX()) != (normalUnitVector.getX() > -0.0))) {
1120 // If not, use a sign-flipped version of the normalUnitVector.
1121 normalUnitVector = new Point2D.Double(-normalUnitVector.getX(), -normalUnitVector.getY());
1122 }
1123
1124 //HACK: swap Y, because the target pixels are top down, but EastNorth is bottom-up.
1125 //This is normally done by MapView.getPoint, but it does not work on vectors.
1126 normalUnitVector.setLocation(normalUnitVector.getX(), -normalUnitVector.getY());
1127 return normalUnitVector;
1128 }
1129
1130 /**
1131 * Determines if from1-to1 and from2-to2 vectors directions are opposite
1132 * @param from1 vector1 start
1133 * @param to1 vector1 end
1134 * @param from2 vector2 start
1135 * @param to2 vector2 end
1136 * @return true if from1-to1 and from2-to2 vectors directions are opposite
1137 */
1138 private static boolean isOppositeDirection(EastNorth from1, EastNorth to1, EastNorth from2, EastNorth to2) {
1139 return (from1.getX()-to1.getX())*(from2.getX()-to2.getX())
1140 +(from1.getY()-to1.getY())*(from2.getY()-to2.getY()) < 0;
1141 }
1142
1143 /**
1144 * Draws right angle symbol at specified position.
1145 * @param g2 the Graphics2D object used to draw on
1146 * @param center center point of angle
1147 * @param normal vector of normal
1148 * @param mirror {@code true} if symbol should be mirrored by the normal
1149 */
1150 private void drawAngleSymbol(Graphics2D g2, Point2D center, Point2D normal, boolean mirror) {
1151 // EastNorth units per pixel
1152 double factor = 1.0/g2.getTransform().getScaleX();
1153 double raoffsetx = symbolSize*factor*normal.getX();
1154 double raoffsety = symbolSize*factor*normal.getY();
1155
1156 double cx = center.getX(), cy = center.getY();
1157 double k = mirror ? -1 : 1;
1158 Point2D ra1 = new Point2D.Double(cx + raoffsetx, cy + raoffsety);
1159 Point2D ra3 = new Point2D.Double(cx - raoffsety*k, cy + raoffsetx*k);
1160 Point2D ra2 = new Point2D.Double(ra1.getX() - raoffsety*k, ra1.getY() + raoffsetx*k);
1161
1162 GeneralPath ra = new GeneralPath();
1163 ra.moveTo((float) ra1.getX(), (float) ra1.getY());
1164 ra.lineTo((float) ra2.getX(), (float) ra2.getY());
1165 ra.lineTo((float) ra3.getX(), (float) ra3.getY());
1166 g2.setStroke(helperStrokeRA);
1167 g2.draw(ra);
1168 }
1169
1170 /**
1171 * Draws given reference segment.
1172 * @param g2 the Graphics2D object used to draw on
1173 * @param mv map view
1174 * @param seg the reference segment
1175 */
1176 private void drawReferenceSegment(Graphics2D g2, MapView mv, ReferenceSegment seg) {
1177 g2.setColor(helperColor);
1178 g2.setStroke(helperStrokeDash);
1179 g2.draw(new MapViewPath(mv).moveTo(seg.p1).lineTo(seg.p2));
1180 }
1181
1182 /**
1183 * Creates a new Line that extends off the edge of the viewport in one direction
1184 * @param start The start point of the line
1185 * @param unitvector A unit vector denoting the direction of the line
1186 * @param g the Graphics2D object it will be used on
1187 * @return created line
1188 */
1189 private static Line2D createSemiInfiniteLine(Point2D start, Point2D unitvector, Graphics2D g) {
1190 Rectangle bounds = g.getClipBounds();
1191 try {
1192 AffineTransform invtrans = g.getTransform().createInverse();
1193 Point2D widthpoint = invtrans.deltaTransform(new Point2D.Double(bounds.width, 0), null);
1194 Point2D heightpoint = invtrans.deltaTransform(new Point2D.Double(0, bounds.height), null);
1195
1196 // Here we should end up with a gross overestimate of the maximum viewport diagonal in what
1197 // Graphics2D calls 'user space'. Essentially a manhattan distance of manhattan distances.
1198 // This can be used as a safe length of line to generate which will always go off-viewport.
1199 double linelength = Math.abs(widthpoint.getX()) + Math.abs(widthpoint.getY())
1200 + Math.abs(heightpoint.getX()) + Math.abs(heightpoint.getY());
1201
1202 return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * linelength), start.getY()
1203 + (unitvector.getY() * linelength)));
1204 } catch (NoninvertibleTransformException e) {
1205 Logging.debug(e);
1206 return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * 10), start.getY()
1207 + (unitvector.getY() * 10)));
1208 }
1209 }
1210}
Note: See TracBrowser for help on using the repository browser.