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

Last change on this file since 7668 was 7668, checked in by stoecker, 10 years ago

cleanup icons, mark undetected icons, set proper mimetype, delete unused icons, update geticons script

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