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

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

code cleanup / robustness in edit layer handling

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