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

Last change on this file since 6037 was 6037, checked in by akks, 11 years ago

see #8470: extrude mode option - extrude.drag-nodes-without-ctrl

  • Property svn:eol-style set to native
File size: 33.8 KB
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
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.AWTEvent;
9import java.awt.BasicStroke;
10import java.awt.Color;
11import java.awt.Cursor;
12import java.awt.Graphics2D;
13import java.awt.Point;
14import java.awt.Rectangle;
15import java.awt.Stroke;
16import java.awt.Toolkit;
17import java.awt.event.AWTEventListener;
18import java.awt.event.ActionEvent;
19import java.awt.event.InputEvent;
20import java.awt.event.KeyEvent;
21import java.awt.event.MouseEvent;
22import java.awt.geom.AffineTransform;
23import java.awt.geom.GeneralPath;
24import java.awt.geom.Line2D;
25import java.awt.geom.NoninvertibleTransformException;
26import java.awt.geom.Point2D;
27import java.util.ArrayList;
28import java.util.Collection;
29import java.util.LinkedList;
30import java.util.List;
31
32import org.openstreetmap.josm.Main;
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.MapFrame;
46import org.openstreetmap.josm.gui.MapView;
47import org.openstreetmap.josm.gui.layer.Layer;
48import org.openstreetmap.josm.gui.layer.MapViewPaintable;
49import org.openstreetmap.josm.gui.layer.OsmDataLayer;
50import org.openstreetmap.josm.gui.util.GuiHelper;
51import org.openstreetmap.josm.tools.Geometry;
52import org.openstreetmap.josm.tools.ImageProvider;
53import org.openstreetmap.josm.tools.Shortcut;
54
55/**
56 * Makes a rectangle from a line, or modifies a rectangle.
57 */
58public class ExtrudeAction extends MapMode implements MapViewPaintable {
59
60 enum Mode { extrude, translate, select, create_new, translate_node }
61
62 private Mode mode = Mode.select;
63
64 /**
65 * If true, when extruding create new node even if segments parallel.
66 */
67 private boolean alwaysCreateNodes = false;
68 private boolean nodeDragWithoutCtrl;
69
70 private long mouseDownTime = 0;
71 private WaySegment selectedSegment = null;
72 private Node selectedNode = null;
73 private Color mainColor;
74 private Stroke mainStroke;
75
76 /**
77 * drawing settings for helper lines
78 */
79 private Color helperColor;
80 private Stroke helperStrokeDash;
81 private Stroke helperStrokeRA;
82
83 private Stroke oldLineStroke;
84 private double symbolSize;
85 /**
86 * Possible directions to move to.
87 */
88 private List<ReferenceSegment> possibleMoveDirections;
89
90
91 /**
92 * Collection of nodes that is moved
93 */
94 private Collection<OsmPrimitive> movingNodeList;
95
96 /**
97 * The direction that is currently active.
98 */
99 private ReferenceSegment activeMoveDirection;
100
101 /**
102 * The position of the mouse cursor when the drag action was initiated.
103 */
104 private Point initialMousePos;
105 /**
106 * The time which needs to pass between click and release before something
107 * counts as a move, in milliseconds
108 */
109 private int initialMoveDelay = 200;
110 /**
111 * The initial EastNorths of node1 and node2
112 */
113 private EastNorth initialN1en;
114 private EastNorth initialN2en;
115 /**
116 * The new EastNorths of node1 and node2
117 */
118 private EastNorth newN1en;
119 private EastNorth newN2en;
120
121 /**
122 * the command that performed last move.
123 */
124 private MoveCommand moveCommand;
125
126 /** The cursor for the 'create_new' mode. */
127 private final Cursor cursorCreateNew;
128
129 /** The cursor for the 'translate' mode. */
130 private final Cursor cursorTranslate;
131
132 /** The cursor for the 'alwaysCreateNodes' submode. */
133 private final Cursor cursorCreateNodes;
134
135 private class ReferenceSegment {
136 public final EastNorth en;
137 public final EastNorth p1;
138 public final EastNorth p2;
139 public final boolean perpendicular;
140
141 public ReferenceSegment(EastNorth en, EastNorth p1, EastNorth p2, boolean perpendicular) {
142 this.en = en;
143 this.p1 = p1;
144 this.p2 = p2;
145 this.perpendicular = perpendicular;
146 }
147 }
148
149 /**
150 * This listener is used to indicate the 'create_new' mode, if the Alt modifier is pressed.
151 */
152 private final AWTEventListener altKeyListener = new AWTEventListener() {
153 @Override
154 public void eventDispatched(AWTEvent e) {
155 if(Main.map == null || Main.map.mapView == null || !Main.map.mapView.isActiveLayerDrawable())
156 return;
157 InputEvent ie = (InputEvent) e;
158 boolean alt = (ie.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0;
159 boolean ctrl = (ie.getModifiers() & (ActionEvent.CTRL_MASK)) != 0;
160 boolean shift = (ie.getModifiers() & (ActionEvent.SHIFT_MASK)) != 0;
161 if (mode == Mode.select) {
162 Main.map.mapView.setNewCursor(ctrl ? cursorTranslate : alt ? cursorCreateNew : shift ? cursorCreateNodes : cursor, this);
163 }
164 }
165 };
166
167 /**
168 * Create a new SelectAction
169 * @param mapFrame The MapFrame this action belongs to.
170 */
171 public ExtrudeAction(MapFrame mapFrame) {
172 super(tr("Extrude"), "extrude/extrude", tr("Create areas"),
173 Shortcut.registerShortcut("mapmode:extrude", tr("Mode: {0}", tr("Extrude")), KeyEvent.VK_X, Shortcut.DIRECT),
174 mapFrame,
175 ImageProvider.getCursor("normal", "rectangle"));
176 putValue("help", ht("/Action/Extrude"));
177 cursorCreateNew = ImageProvider.getCursor("normal", "rectangle_plus");
178 cursorTranslate = ImageProvider.getCursor("normal", "rectangle_move");
179 cursorCreateNodes = ImageProvider.getCursor("normal", "rectangle_plussmall");
180 }
181
182 @Override public String getModeHelpText() {
183 if (mode == Mode.translate)
184 return tr("Move a segment along its normal, then release the mouse button.");
185 else if (mode == Mode.extrude)
186 return tr("Draw a rectangle of the desired size, then release the mouse button.");
187 else if (mode == Mode.create_new)
188 return tr("Draw a rectangle of the desired size, then release the mouse button.");
189 else
190 return tr("Drag a way segment to make a rectangle. Ctrl-drag to move a segment along its normal, " +
191 "Alt-drag to create a new rectangle, double click to add a new node.");
192 }
193
194 @Override public boolean layerIsSupported(Layer l) {
195 return l instanceof OsmDataLayer;
196 }
197
198 @Override public void enterMode() {
199 super.enterMode();
200 Main.map.mapView.addMouseListener(this);
201 Main.map.mapView.addMouseMotionListener(this);
202 try {
203 Toolkit.getDefaultToolkit().addAWTEventListener(altKeyListener, AWTEvent.KEY_EVENT_MASK);
204 } catch (SecurityException ex) {
205 }
206 initialMoveDelay = Main.pref.getInteger("edit.initial-move-delay",200);
207 mainColor = Main.pref.getColor(marktr("Extrude: main line"), null);
208 if (mainColor == null) mainColor = PaintColors.SELECTED.get();
209 helperColor = Main.pref.getColor(marktr("Extrude: helper line"), Color.ORANGE);
210 helperStrokeDash = GuiHelper.getCustomizedStroke(Main.pref.get("extrude.stroke.helper-line", "1 4"));
211 helperStrokeRA = new BasicStroke(1);
212 symbolSize = Main.pref.getDouble("extrude.angle-symbol-radius", 8);
213 nodeDragWithoutCtrl = Main.pref.getBoolean("extrude.drag-nodes-without-ctrl", true);
214 oldLineStroke = GuiHelper.getCustomizedStroke(Main.pref.get("extrude.ctrl.stroke.old-line", "1"));
215 mainStroke = GuiHelper.getCustomizedStroke(Main.pref.get("extrude.stroke.main", "3"));
216 }
217
218 @Override public void exitMode() {
219 Main.map.mapView.removeMouseListener(this);
220 Main.map.mapView.removeMouseMotionListener(this);
221 Main.map.mapView.removeTemporaryLayer(this);
222 try {
223 Toolkit.getDefaultToolkit().removeAWTEventListener(altKeyListener);
224 } catch (SecurityException ex) {
225 }
226 super.exitMode();
227 }
228
229 /**
230 * If the left mouse button is pressed over a segment, switch
231 * to either extrude, translate or create_new mode depending on whether Ctrl or Alt is held.
232 */
233 @Override public void mousePressed(MouseEvent e) {
234 if(!Main.map.mapView.isActiveLayerVisible())
235 return;
236 if (!(Boolean)this.getValue("active"))
237 return;
238 if (e.getButton() != MouseEvent.BUTTON1)
239 return;
240
241 requestFocusInMapView();
242 updateKeyModifiers(e);
243
244 selectedNode = Main.map.mapView.getNearestNode(e.getPoint(), OsmPrimitive.isSelectablePredicate);
245 selectedSegment = Main.map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive.isSelectablePredicate);
246
247 // If nothing gets caught, stay in select mode
248 if (selectedSegment == null && selectedNode == null) return;
249
250 if (selectedNode != null) {
251 if (ctrl || nodeDragWithoutCtrl) {
252 movingNodeList = new ArrayList<OsmPrimitive>();
253 movingNodeList.add(selectedNode);
254 calculatePossibleDirectionsByNode();
255 if (possibleMoveDirections.isEmpty()) {
256 // if no directions fould, do not enter dragging mode
257 return;
258 }
259 mode = Mode.translate_node;
260 }
261 } else {
262 // Otherwise switch to another mode
263 if (ctrl) {
264 mode = Mode.translate;
265 movingNodeList = new ArrayList<OsmPrimitive>();
266 movingNodeList.add(selectedSegment.getFirstNode());
267 movingNodeList.add(selectedSegment.getSecondNode());
268 } else if (alt) {
269 mode = Mode.create_new;
270 // create a new segment and then select and extrude the new segment
271 getCurrentDataSet().setSelected(selectedSegment.way);
272 alwaysCreateNodes = true;
273 } else {
274 mode = Mode.extrude;
275 getCurrentDataSet().setSelected(selectedSegment.way);
276 alwaysCreateNodes = shift;
277 }
278 calculatePossibleDirectionsBySegment();
279 }
280
281 // Signifies that nothing has happened yet
282 newN1en = null;
283 newN2en = null;
284 moveCommand = null;
285
286 Main.map.mapView.addTemporaryLayer(this);
287
288 updateStatusLine();
289 Main.map.mapView.repaint();
290
291 // Make note of time pressed
292 mouseDownTime = System.currentTimeMillis();
293
294 // Make note of mouse position
295 initialMousePos = e.getPoint();
296 }
297
298 /**
299 * Perform action depending on what mode we're in.
300 */
301 @Override public void mouseDragged(MouseEvent e) {
302 if(!Main.map.mapView.isActiveLayerVisible())
303 return;
304
305 // do not count anything as a drag if it lasts less than 100 milliseconds.
306 if (System.currentTimeMillis() - mouseDownTime < initialMoveDelay)
307 return;
308
309 if (mode == Mode.select) {
310 // Just sit tight and wait for mouse to be released.
311 } else {
312 //move, create new and extrude mode - move the selected segment
313
314 EastNorth mouseEn = Main.map.mapView.getEastNorth(e.getPoint().x, e.getPoint().y);
315 EastNorth bestMovement = calculateBestMovement(mouseEn);
316
317 newN1en = new EastNorth(initialN1en.getX() + bestMovement.getX(), initialN1en.getY() + bestMovement.getY());
318 newN2en = new EastNorth(initialN2en.getX() + bestMovement.getX(), initialN2en.getY() + bestMovement.getY());
319
320 // find out the movement distance, in metres
321 double distance = Main.getProjection().eastNorth2latlon(initialN1en).greatCircleDistance(Main.getProjection().eastNorth2latlon(newN1en));
322 Main.map.statusLine.setDist(distance);
323 updateStatusLine();
324
325 Main.map.mapView.setNewCursor(Cursor.MOVE_CURSOR, this);
326
327 if (mode == Mode.extrude || mode == Mode.create_new) {
328 //nothing here
329 } else if (mode == Mode.translate_node || mode == Mode.translate) {
330 //move nodes to new position
331 if (moveCommand == null) {
332 //make a new move command
333 moveCommand = new MoveCommand(movingNodeList, bestMovement.getX(), bestMovement.getY());
334 Main.main.undoRedo.add(moveCommand);
335 } else {
336 //reuse existing move command
337 moveCommand.moveAgainTo(bestMovement.getX(), bestMovement.getY());
338 }
339 }
340
341 Main.map.mapView.repaint();
342 }
343 }
344
345 /**
346 * Do anything that needs to be done, then switch back to select mode
347 */
348 @Override public void mouseReleased(MouseEvent e) {
349
350 if(!Main.map.mapView.isActiveLayerVisible())
351 return;
352
353 if (mode == Mode.select) {
354 // Nothing to be done
355 } else {
356 if (mode == Mode.create_new) {
357 if (e.getPoint().distance(initialMousePos) > 10 && newN1en != null) {
358 createNewRectangle();
359 }
360 } else if (mode == Mode.extrude) {
361 if( e.getClickCount() == 2 && e.getPoint().equals(initialMousePos) ) {
362 // double click adds a new node
363 addNewNode(e);
364 }
365 else if (e.getPoint().distance(initialMousePos) > 10 && newN1en != null && selectedSegment != null) {
366 // main extrusion commands
367 performExtrusion();
368 }
369 } else if (mode == Mode.translate || mode == Mode.translate_node) {
370 //Commit translate
371 //the move command is already committed in mouseDragged
372 }
373
374 boolean alt = (e.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0;
375 boolean ctrl = (e.getModifiers() & (ActionEvent.CTRL_MASK)) != 0;
376 boolean shift = (e.getModifiers() & (ActionEvent.SHIFT_MASK)) != 0;
377 // Switch back into select mode
378 Main.map.mapView.setNewCursor(ctrl ? cursorTranslate : alt ? cursorCreateNew : shift ? cursorCreateNodes : cursor, this);
379 Main.map.mapView.removeTemporaryLayer(this);
380 selectedSegment = null;
381 moveCommand = null;
382 mode = Mode.select;
383
384 updateStatusLine();
385 Main.map.mapView.repaint();
386 }
387 }
388
389 /**
390 * Insert node into nearby segment
391 * @param e - current mouse point
392 */
393 private void addNewNode(MouseEvent e) {
394 // Should maybe do the same as in DrawAction and fetch all nearby segments?
395 WaySegment ws = Main.map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive.isSelectablePredicate);
396 if (ws != null) {
397 Node n = new Node(Main.map.mapView.getLatLon(e.getX(), e.getY()));
398 EastNorth A = ws.getFirstNode().getEastNorth();
399 EastNorth B = ws.getSecondNode().getEastNorth();
400 n.setEastNorth(Geometry.closestPointToSegment(A, B, n.getEastNorth()));
401 Way wnew = new Way(ws.way);
402 wnew.addNode(ws.lowerIndex+1, n);
403 SequenceCommand cmds = new SequenceCommand(tr("Add a new node to an existing way"),
404 new AddCommand(n), new ChangeCommand(ws.way, wnew));
405 Main.main.undoRedo.add(cmds);
406 }
407 }
408
409 private void createNewRectangle() {
410 if (selectedSegment == null) return;
411 // crete a new rectangle
412 Collection<Command> cmds = new LinkedList<Command>();
413 Node third = new Node(newN2en);
414 Node fourth = new Node(newN1en);
415 Way wnew = new Way();
416 wnew.addNode(selectedSegment.getFirstNode());
417 wnew.addNode(selectedSegment.getSecondNode());
418 wnew.addNode(third);
419 wnew.addNode(fourth);
420 // ... and close the way
421 wnew.addNode(selectedSegment.getFirstNode());
422 // undo support
423 cmds.add(new AddCommand(third));
424 cmds.add(new AddCommand(fourth));
425 cmds.add(new AddCommand(wnew));
426 Command c = new SequenceCommand(tr("Extrude Way"), cmds);
427 Main.main.undoRedo.add(c);
428 getCurrentDataSet().setSelected(wnew);
429 }
430
431 /**
432 * Do actual extrusion of @field selectedSegment
433 */
434 private void performExtrusion() {
435 // create extrusion
436 Collection<Command> cmds = new LinkedList<Command>();
437 Way wnew = new Way(selectedSegment.way);
438 boolean wayWasModified = false;
439 boolean wayWasSingleSegment = wnew.getNodesCount() == 2;
440 int insertionPoint = selectedSegment.lowerIndex + 1;
441
442 //find if the new points overlap existing segments (in case of 90 degree angles)
443 Node prevNode = getPreviousNode(selectedSegment.lowerIndex);
444 boolean nodeOverlapsSegment = prevNode != null && Geometry.segmentsParallel(initialN1en, prevNode.getEastNorth(), initialN1en, newN1en);
445 boolean hasOtherWays = this.hasNodeOtherWays(selectedSegment.getFirstNode(), selectedSegment.way);
446
447 if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) {
448 //move existing node
449 Node n1Old = selectedSegment.getFirstNode();
450 cmds.add(new MoveCommand(n1Old, Main.getProjection().eastNorth2latlon(newN1en)));
451 } else {
452 //introduce new node
453 Node n1New = new Node(Main.getProjection().eastNorth2latlon(newN1en));
454 wnew.addNode(insertionPoint, n1New);
455 wayWasModified = true;
456 insertionPoint ++;
457 cmds.add(new AddCommand(n1New));
458 }
459
460 //find if the new points overlap existing segments (in case of 90 degree angles)
461 Node nextNode = getNextNode(selectedSegment.lowerIndex + 1);
462 nodeOverlapsSegment = nextNode != null && Geometry.segmentsParallel(initialN2en, nextNode.getEastNorth(), initialN2en, newN2en);
463 hasOtherWays = hasNodeOtherWays(selectedSegment.getSecondNode(), selectedSegment.way);
464
465 if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) {
466 //move existing node
467 Node n2Old = selectedSegment.getSecondNode();
468 cmds.add(new MoveCommand(n2Old, Main.getProjection().eastNorth2latlon(newN2en)));
469 } else {
470 //introduce new node
471 Node n2New = new Node(Main.getProjection().eastNorth2latlon(newN2en));
472 wnew.addNode(insertionPoint, n2New);
473 wayWasModified = true;
474 insertionPoint ++;
475 cmds.add(new AddCommand(n2New));
476 }
477
478 //the way was a single segment, close the way
479 if (wayWasSingleSegment) {
480 wnew.addNode(selectedSegment.getFirstNode());
481 wayWasModified = true;
482 }
483 if (wayWasModified) {
484 // we only need to change the way if its node list was really modified
485 cmds.add(new ChangeCommand(selectedSegment.way, wnew));
486 }
487 Command c = new SequenceCommand(tr("Extrude Way"), cmds);
488 Main.main.undoRedo.add(c);
489 }
490
491 /**
492 * This method tests if a node has other ways apart from the given one.
493 * @param node
494 * @param myWay
495 * @return true of node belongs only to myWay, false if there are more ways.
496 */
497 private boolean hasNodeOtherWays(Node node, Way myWay) {
498 for (OsmPrimitive p : node.getReferrers()) {
499 if (p instanceof Way && p.isUsable() && p != myWay)
500 return true;
501 }
502 return false;
503 }
504
505 /**
506 * Determine best movenemnt from initialMousePos to current position @param mouseEn,
507 * choosing one of the directions @field possibleMoveDirections
508 * @return movement vector
509 */
510 private EastNorth calculateBestMovement(EastNorth mouseEn) {
511
512 EastNorth initialMouseEn = Main.map.mapView.getEastNorth(initialMousePos.x, initialMousePos.y);
513 EastNorth mouseMovement = initialMouseEn.sub(mouseEn);
514
515 double bestDistance = Double.POSITIVE_INFINITY;
516 EastNorth bestMovement = null;
517 activeMoveDirection = null;
518
519 //find the best movement direction and vector
520 for (ReferenceSegment direction : possibleMoveDirections) {
521 EastNorth movement = calculateSegmentOffset(initialN1en, initialN2en, direction.en, mouseEn);
522 if (movement == null) {
523 //if direction parallel to segment.
524 continue;
525 }
526
527 double distanceFromMouseMovement = movement.distance(mouseMovement);
528 if (bestDistance > distanceFromMouseMovement) {
529 bestDistance = distanceFromMouseMovement;
530 activeMoveDirection = direction;
531 bestMovement = movement;
532 }
533 }
534 return bestMovement;
535 }
536
537 /***
538 * This method calculates offset amount by witch to move the given segment perpendicularly for it to be in line with mouse position.
539 * @param segmentP1
540 * @param segmentP2
541 * @param targetPos
542 * @return offset amount of P1 and P2.
543 */
544 private static EastNorth calculateSegmentOffset(EastNorth segmentP1, EastNorth segmentP2, EastNorth moveDirection,
545 EastNorth targetPos) {
546 EastNorth intersectionPoint;
547 if (segmentP1.distanceSq(segmentP2)>1e-7) {
548 intersectionPoint = Geometry.getLineLineIntersection(segmentP1, segmentP2, targetPos, targetPos.add(moveDirection));
549 } else {
550 intersectionPoint = Geometry.closestPointToLine(targetPos, targetPos.add(moveDirection), segmentP1);
551 }
552
553 if (intersectionPoint == null)
554 return null;
555 else
556 //return distance form base to target position
557 return intersectionPoint.sub(targetPos);
558 }
559
560 /**
561 * Gather possible move directions - perpendicular to the selected segment and parallel to neighbor segments
562 */
563 private void calculatePossibleDirectionsBySegment() {
564 // remember initial positions for segment nodes.
565 initialN1en = selectedSegment.getFirstNode().getEastNorth();
566 initialN2en = selectedSegment.getSecondNode().getEastNorth();
567
568 //add direction perpendicular to the selected segment
569 possibleMoveDirections = new ArrayList<ReferenceSegment>();
570 possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
571 initialN1en.getY() - initialN2en.getY(),
572 initialN2en.getX() - initialN1en.getX()
573 ), initialN1en, initialN2en, true));
574
575
576 //add directions parallel to neighbor segments
577 Node prevNode = getPreviousNode(selectedSegment.lowerIndex);
578 if (prevNode != null) {
579 EastNorth en = prevNode.getEastNorth();
580 possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
581 initialN1en.getX() - en.getX(),
582 initialN1en.getY() - en.getY()
583 ), initialN1en, en, false));
584 }
585
586 Node nextNode = getNextNode(selectedSegment.lowerIndex + 1);
587 if (nextNode != null) {
588 EastNorth en = nextNode.getEastNorth();
589 possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
590 initialN2en.getX() - en.getX(),
591 initialN2en.getY() - en.getY()
592 ), initialN2en, en, false));
593 }
594 }
595
596 /**
597 * Gather possible move directions - along all adjacent segments
598 */
599 private void calculatePossibleDirectionsByNode() {
600 // remember initial positions for segment nodes.
601 initialN1en = selectedNode.getEastNorth();
602 initialN2en = initialN1en;
603 possibleMoveDirections = new ArrayList<ReferenceSegment>();
604 for (OsmPrimitive p: selectedNode.getReferrers()) {
605 if (p instanceof Way && p.isUsable()) {
606 for (Node neighbor: ((Way) p).getNeighbours(selectedNode)) {
607 EastNorth en = neighbor.getEastNorth();
608 possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
609 initialN1en.getX() - en.getX(),
610 initialN1en.getY() - en.getY()
611 ), initialN1en, en, false));
612 }
613 }
614 }
615 }
616
617 /**
618 * Gets a node from selected way before given index.
619 * @param index index of current node
620 * @return index of previous node or -1 if there are no nodes there.
621 */
622 private int getPreviousNodeIndex(int index) {
623 if (index > 0)
624 return index - 1;
625 else if (selectedSegment.way.isClosed())
626 return selectedSegment.way.getNodesCount() - 2;
627 else
628 return -1;
629 }
630
631 /**
632 * Gets a node from selected way before given index.
633 * @param index index of current node
634 * @return previous node or null if there are no nodes there.
635 */
636 private Node getPreviousNode(int index) {
637 int indexPrev = getPreviousNodeIndex(index);
638 if (indexPrev >= 0)
639 return selectedSegment.way.getNode(indexPrev);
640 else
641 return null;
642 }
643
644
645 /**
646 * Gets a node from selected way after given index.
647 * @param index index of current node
648 * @return index of next node or -1 if there are no nodes there.
649 */
650 private int getNextNodeIndex(int index) {
651 int count = selectedSegment.way.getNodesCount();
652 if (index < count - 1)
653 return index + 1;
654 else if (selectedSegment.way.isClosed())
655 return 1;
656 else
657 return -1;
658 }
659
660 /**
661 * Gets a node from selected way after given index.
662 * @param index index of current node
663 * @return next node or null if there are no nodes there.
664 */
665 private Node getNextNode(int index) {
666 int indexNext = getNextNodeIndex(index);
667 if (indexNext >= 0)
668 return selectedSegment.way.getNode(indexNext);
669 else
670 return null;
671 }
672
673 @Override
674 public void paint(Graphics2D g, MapView mv, Bounds box) {
675 Graphics2D g2 = g;
676 if (mode == Mode.select) {
677 // Nothing to do
678 } else {
679 if (newN1en != null) {
680
681 Point p1 = mv.getPoint(initialN1en);
682 Point p2 = mv.getPoint(initialN2en);
683 Point p3 = mv.getPoint(newN1en);
684 Point p4 = mv.getPoint(newN2en);
685
686 EastNorth normalUnitVector = getNormalUniVector();
687
688 if (mode == Mode.extrude || mode == Mode.create_new) {
689 g2.setColor(mainColor);
690 g2.setStroke(mainStroke);
691 // Draw rectangle around new area.
692 GeneralPath b = new GeneralPath();
693 b.moveTo(p1.x, p1.y); b.lineTo(p3.x, p3.y);
694 b.lineTo(p4.x, p4.y); b.lineTo(p2.x, p2.y);
695 b.lineTo(p1.x, p1.y);
696 g2.draw(b);
697
698 if (activeMoveDirection != null) {
699 // Draw reference way
700 Point pr1 = mv.getPoint(activeMoveDirection.p1);
701 Point pr2 = mv.getPoint(activeMoveDirection.p2);
702 b = new GeneralPath();
703 b.moveTo(pr1.x, pr1.y);
704 b.lineTo(pr2.x, pr2.y);
705 g2.setColor(helperColor);
706 g2.setStroke(helperStrokeDash);
707 g2.draw(b);
708
709 // Draw right angle marker on first node position, only when moving at right angle
710 if (activeMoveDirection.perpendicular) {
711 // mirror RightAngle marker, so it is inside the extrude
712 double headingRefWS = activeMoveDirection.p1.heading(activeMoveDirection.p2);
713 double headingMoveDir = Math.atan2(normalUnitVector.getY(), normalUnitVector.getX());
714 double headingDiff = headingRefWS - headingMoveDir;
715 if (headingDiff < 0) headingDiff += 2 * Math.PI;
716 boolean mirrorRA = Math.abs(headingDiff - Math.PI) > 1e-5;
717 drawAngleSymbol(g2, pr1, normalUnitVector, mirrorRA);
718 }
719 }
720 } else if (mode == Mode.translate || mode == Mode.translate_node) {
721 g2.setColor(mainColor);
722 if (p1.distance(p2) < 3) {
723 g2.setStroke(mainStroke);
724 g2.drawOval((int)(p1.x-symbolSize/2), (int)(p1.y-symbolSize/2),
725 (int)(symbolSize), (int)(symbolSize));
726 } else {
727 Line2D oldline = new Line2D.Double(p1, p2);
728 g2.setStroke(oldLineStroke);
729 g2.draw(oldline);
730 }
731
732 if (activeMoveDirection != null) {
733
734 g2.setColor(helperColor);
735 g2.setStroke(helperStrokeDash);
736 // Draw a guideline along the normal.
737 Line2D normline;
738 Point2D centerpoint = new Point2D.Double((p1.getX()+p2.getX())*0.5, (p1.getY()+p2.getY())*0.5);
739 normline = createSemiInfiniteLine(centerpoint, normalUnitVector, g2);
740 g2.draw(normline);
741 // Draw right angle marker on initial position, only when moving at right angle
742 if (activeMoveDirection.perpendicular) {
743 // EastNorth units per pixel
744 g2.setStroke(helperStrokeRA);
745 g2.setColor(mainColor);
746 drawAngleSymbol(g2, centerpoint, normalUnitVector, false);
747 }
748 }
749 }
750 }
751 g2.setStroke(helperStrokeRA); // restore default stroke to prevent starnge occasional drawings
752 }
753 }
754
755 private EastNorth getNormalUniVector() {
756 double fac = 1.0 / activeMoveDirection.en.distance(0,0);
757 // mult by factor to get unit vector.
758 EastNorth normalUnitVector = new EastNorth(activeMoveDirection.en.getX() * fac, activeMoveDirection.en.getY() * fac);
759
760 // Check to see if our new N1 is in a positive direction with respect to the normalUnitVector.
761 // Even if the x component is zero, we should still be able to discern using +0.0 and -0.0
762 if (newN1en != null && ((newN1en.getX() > initialN1en.getX()) != (normalUnitVector.getX() > -0.0))) {
763 // If not, use a sign-flipped version of the normalUnitVector.
764 normalUnitVector = new EastNorth(-normalUnitVector.getX(), -normalUnitVector.getY());
765 }
766
767 //HACK: swap Y, because the target pixels are top down, but EastNorth is bottom-up.
768 //This is normally done by MapView.getPoint, but it does not work on vectors.
769 normalUnitVector.setLocation(normalUnitVector.getX(), -normalUnitVector.getY());
770 return normalUnitVector;
771 }
772
773 private void drawAngleSymbol(Graphics2D g2, Point2D center, EastNorth normal, boolean mirror) {
774 // EastNorth units per pixel
775 double factor = 1.0/g2.getTransform().getScaleX();
776 double raoffsetx = symbolSize*factor*normal.getX();
777 double raoffsety = symbolSize*factor*normal.getY();
778
779 double cx = center.getX(), cy = center.getY();
780 double k = (mirror ? -1 : 1);
781 Point2D ra1 = new Point2D.Double(cx + raoffsetx, cy + raoffsety);
782 Point2D ra3 = new Point2D.Double(cx - raoffsety*k, cy + raoffsetx*k);
783 Point2D ra2 = new Point2D.Double(ra1.getX() - raoffsety*k, ra1.getY() + raoffsetx*k);
784
785 GeneralPath ra = new GeneralPath();
786 ra.moveTo((float)ra1.getX(), (float)ra1.getY());
787 ra.lineTo((float)ra2.getX(), (float)ra2.getY());
788 ra.lineTo((float)ra3.getX(), (float)ra3.getY());
789 g2.setStroke(helperStrokeRA);
790 g2.draw(ra);
791 }
792
793 /**
794 * Create a new Line that extends off the edge of the viewport in one direction
795 * @param start The start point of the line
796 * @param unitvector A unit vector denoting the direction of the line
797 * @param g the Graphics2D object it will be used on
798 */
799 static private Line2D createSemiInfiniteLine(Point2D start, Point2D unitvector, Graphics2D g) {
800 Rectangle bounds = g.getDeviceConfiguration().getBounds();
801 try {
802 AffineTransform invtrans = g.getTransform().createInverse();
803 Point2D widthpoint = invtrans.deltaTransform(new Point2D.Double(bounds.width,0), null);
804 Point2D heightpoint = invtrans.deltaTransform(new Point2D.Double(0,bounds.height), null);
805
806 // Here we should end up with a gross overestimate of the maximum viewport diagonal in what
807 // Graphics2D calls 'user space'. Essentially a manhattan distance of manhattan distances.
808 // This can be used as a safe length of line to generate which will always go off-viewport.
809 double linelength = Math.abs(widthpoint.getX()) + Math.abs(widthpoint.getY()) + Math.abs(heightpoint.getX()) + Math.abs(heightpoint.getY());
810
811 return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * linelength) , start.getY() + (unitvector.getY() * linelength)));
812 }
813 catch (NoninvertibleTransformException e) {
814 return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * 10) , start.getY() + (unitvector.getY() * 10)));
815 }
816 }
817}
Note: See TracBrowser for help on using the repository browser.