001 // License: GPL. Copyright 2007 by Immanuel Scholz and others
002 package org.openstreetmap.josm.actions.mapmode;
003
004 import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005 import static org.openstreetmap.josm.tools.I18n.marktr;
006 import static org.openstreetmap.josm.tools.I18n.tr;
007
008 import java.awt.AWTEvent;
009 import java.awt.BasicStroke;
010 import java.awt.Color;
011 import java.awt.Cursor;
012 import java.awt.Graphics2D;
013 import java.awt.Point;
014 import java.awt.Rectangle;
015 import java.awt.Toolkit;
016 import java.awt.event.AWTEventListener;
017 import java.awt.event.ActionEvent;
018 import java.awt.event.InputEvent;
019 import java.awt.event.KeyEvent;
020 import java.awt.event.MouseEvent;
021 import java.awt.geom.AffineTransform;
022 import java.awt.geom.GeneralPath;
023 import java.awt.geom.Line2D;
024 import java.awt.geom.NoninvertibleTransformException;
025 import java.awt.geom.Point2D;
026 import java.util.ArrayList;
027 import java.util.Collection;
028 import java.util.LinkedList;
029 import java.util.List;
030
031 import org.openstreetmap.josm.Main;
032 import org.openstreetmap.josm.command.AddCommand;
033 import org.openstreetmap.josm.command.ChangeCommand;
034 import org.openstreetmap.josm.command.Command;
035 import org.openstreetmap.josm.command.MoveCommand;
036 import org.openstreetmap.josm.command.SequenceCommand;
037 import org.openstreetmap.josm.data.Bounds;
038 import org.openstreetmap.josm.data.coor.EastNorth;
039 import org.openstreetmap.josm.data.osm.Node;
040 import org.openstreetmap.josm.data.osm.OsmPrimitive;
041 import org.openstreetmap.josm.data.osm.Way;
042 import org.openstreetmap.josm.data.osm.WaySegment;
043 import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
044 import org.openstreetmap.josm.gui.MapFrame;
045 import org.openstreetmap.josm.gui.MapView;
046 import org.openstreetmap.josm.gui.layer.Layer;
047 import org.openstreetmap.josm.gui.layer.MapViewPaintable;
048 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
049 import org.openstreetmap.josm.tools.Geometry;
050 import org.openstreetmap.josm.tools.ImageProvider;
051 import org.openstreetmap.josm.tools.Shortcut;
052
053 /**
054 * Makes a rectangle from a line, or modifies a rectangle.
055 */
056 public class ExtrudeAction extends MapMode implements MapViewPaintable {
057
058 enum Mode { extrude, translate, select, create_new }
059
060 private Mode mode = Mode.select;
061
062 /**
063 * If true, when extruding create new node even if segments parallel.
064 */
065 private boolean alwaysCreateNodes = false;
066
067 private long mouseDownTime = 0;
068 private WaySegment selectedSegment = null;
069 private Color selectedColor;
070
071 /**
072 * drawing settings for helper lines
073 */
074 private Color helperColor;
075 private BasicStroke helperStrokeDash;
076 private BasicStroke helperStrokeRA;
077
078 /**
079 * Possible directions to move to.
080 */
081 private List<ReferenceSegment> possibleMoveDirections;
082
083 /**
084 * The direction that is currently active.
085 */
086 private ReferenceSegment activeMoveDirection;
087
088 /**
089 * The position of the mouse cursor when the drag action was initiated.
090 */
091 private Point initialMousePos;
092 /**
093 * The time which needs to pass between click and release before something
094 * counts as a move, in milliseconds
095 */
096 private int initialMoveDelay = 200;
097 /**
098 * The initial EastNorths of node1 and node2
099 */
100 private EastNorth initialN1en;
101 private EastNorth initialN2en;
102 /**
103 * The new EastNorths of node1 and node2
104 */
105 private EastNorth newN1en;
106 private EastNorth newN2en;
107
108 /**
109 * the command that performed last move.
110 */
111 private MoveCommand moveCommand;
112
113 /** The cursor for the 'create_new' mode. */
114 private final Cursor cursorCreateNew;
115
116 /** The cursor for the 'translate' mode. */
117 private final Cursor cursorTranslate;
118
119 /** The cursor for the 'alwaysCreateNodes' submode. */
120 private final Cursor cursorCreateNodes;
121
122 private class ReferenceSegment {
123 public final EastNorth en;
124 public final WaySegment ws;
125 public final boolean perpendicular;
126
127 public ReferenceSegment(EastNorth en, WaySegment ws, boolean perpendicular) {
128 this.en = en;
129 this.ws = ws;
130 this.perpendicular = perpendicular;
131 }
132 }
133
134 /**
135 * This listener is used to indicate the 'create_new' mode, if the Alt modifier is pressed.
136 */
137 private final AWTEventListener altKeyListener = new AWTEventListener() {
138 @Override
139 public void eventDispatched(AWTEvent e) {
140 if(Main.map == null || Main.map.mapView == null || !Main.map.mapView.isActiveLayerDrawable())
141 return;
142 InputEvent ie = (InputEvent) e;
143 boolean alt = (ie.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0;
144 boolean ctrl = (ie.getModifiers() & (ActionEvent.CTRL_MASK)) != 0;
145 boolean shift = (ie.getModifiers() & (ActionEvent.SHIFT_MASK)) != 0;
146 if (mode == Mode.select) {
147 Main.map.mapView.setNewCursor(ctrl ? cursorTranslate : alt ? cursorCreateNew : shift ? cursorCreateNodes : cursor, this);
148 }
149 }
150 };
151
152 /**
153 * Create a new SelectAction
154 * @param mapFrame The MapFrame this action belongs to.
155 */
156 public ExtrudeAction(MapFrame mapFrame) {
157 super(tr("Extrude"), "extrude/extrude", tr("Create areas"),
158 Shortcut.registerShortcut("mapmode:extrude", tr("Mode: {0}", tr("Extrude")), KeyEvent.VK_X, Shortcut.DIRECT),
159 mapFrame,
160 ImageProvider.getCursor("normal", "rectangle"));
161 putValue("help", ht("/Action/Extrude"));
162 initialMoveDelay = Main.pref.getInteger("edit.initial-move-delay",200);
163 selectedColor = PaintColors.SELECTED.get();
164 cursorCreateNew = ImageProvider.getCursor("normal", "rectangle_plus");
165 cursorTranslate = ImageProvider.getCursor("normal", "rectangle_move");
166 cursorCreateNodes = ImageProvider.getCursor("normal", "rectangle_plussmall");
167 helperColor = Main.pref.getColor(marktr("Extrude: helper line"), Color.ORANGE);
168 float dash1[] = { 4.0f };
169 helperStrokeDash = new BasicStroke(1.0f, BasicStroke.CAP_BUTT,
170 BasicStroke.JOIN_MITER, 10.0f, dash1, 0.0f);
171 helperStrokeRA = new BasicStroke(1);
172 }
173
174 @Override public String getModeHelpText() {
175 if (mode == Mode.translate)
176 return tr("Move a segment along its normal, then release the mouse button.");
177 else if (mode == Mode.extrude)
178 return tr("Draw a rectangle of the desired size, then release the mouse button.");
179 else if (mode == Mode.create_new)
180 return tr("Draw a rectangle of the desired size, then release the mouse button.");
181 else
182 return tr("Drag a way segment to make a rectangle. Ctrl-drag to move a segment along its normal, " +
183 "Alt-drag to create a new rectangle, double click to add a new node.");
184 }
185
186 @Override public boolean layerIsSupported(Layer l) {
187 return l instanceof OsmDataLayer;
188 }
189
190 @Override public void enterMode() {
191 super.enterMode();
192 Main.map.mapView.addMouseListener(this);
193 Main.map.mapView.addMouseMotionListener(this);
194 try {
195 Toolkit.getDefaultToolkit().addAWTEventListener(altKeyListener, AWTEvent.KEY_EVENT_MASK);
196 } catch (SecurityException ex) {
197 }
198 }
199
200 @Override public void exitMode() {
201 Main.map.mapView.removeMouseListener(this);
202 Main.map.mapView.removeMouseMotionListener(this);
203 Main.map.mapView.removeTemporaryLayer(this);
204 try {
205 Toolkit.getDefaultToolkit().removeAWTEventListener(altKeyListener);
206 } catch (SecurityException ex) {
207 }
208 super.exitMode();
209 }
210
211 /**
212 * If the left mouse button is pressed over a segment, switch
213 * to either extrude, translate or create_new mode depending on whether Ctrl or Alt is held.
214 */
215 @Override public void mousePressed(MouseEvent e) {
216 if(!Main.map.mapView.isActiveLayerVisible())
217 return;
218 if (!(Boolean)this.getValue("active"))
219 return;
220 if (e.getButton() != MouseEvent.BUTTON1)
221 return;
222
223 updateKeyModifiers(e);
224
225 selectedSegment = Main.map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive.isSelectablePredicate);
226
227 if (selectedSegment == null) {
228 // If nothing gets caught, stay in select mode
229 } else {
230 // Otherwise switch to another mode
231
232 if (ctrl) {
233 mode = Mode.translate;
234 } else if (alt) {
235 mode = Mode.create_new;
236 // create a new segment and then select and extrude the new segment
237 getCurrentDataSet().setSelected(selectedSegment.way);
238 alwaysCreateNodes = true;
239 } else {
240 mode = Mode.extrude;
241 getCurrentDataSet().setSelected(selectedSegment.way);
242 alwaysCreateNodes = shift;
243 }
244
245 // remember initial positions for segment nodes.
246 initialN1en = selectedSegment.getFirstNode().getEastNorth();
247 initialN2en = selectedSegment.getSecondNode().getEastNorth();
248
249 //gather possible move directions - perpendicular to the selected segment and parallel to neighbor segments
250 possibleMoveDirections = new ArrayList<ReferenceSegment>();
251 possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
252 initialN1en.getY() - initialN2en.getY(),
253 initialN2en.getX() - initialN1en.getX()
254 ), selectedSegment, true));
255
256 //add directions parallel to neighbor segments
257
258 Node prevNode = getPreviousNode(selectedSegment.lowerIndex);
259 if (prevNode != null) {
260 EastNorth en = prevNode.getEastNorth();
261 possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
262 initialN1en.getX() - en.getX(),
263 initialN1en.getY() - en.getY()
264 ), new WaySegment(selectedSegment.way, getPreviousNodeIndex(selectedSegment.lowerIndex)), false));
265 }
266
267 Node nextNode = getNextNode(selectedSegment.lowerIndex + 1);
268 if (nextNode != null) {
269 EastNorth en = nextNode.getEastNorth();
270 possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
271 initialN2en.getX() - en.getX(),
272 initialN2en.getY() - en.getY()
273 ), new WaySegment(selectedSegment.way, getPreviousNodeIndex(getNextNodeIndex(getNextNodeIndex(selectedSegment.lowerIndex)))), false));
274 }
275
276 // Signifies that nothing has happened yet
277 newN1en = null;
278 newN2en = null;
279 moveCommand = null;
280
281 Main.map.mapView.addTemporaryLayer(this);
282
283 updateStatusLine();
284 Main.map.mapView.repaint();
285
286 // Make note of time pressed
287 mouseDownTime = System.currentTimeMillis();
288
289 // Make note of mouse position
290 initialMousePos = e.getPoint();
291 }
292 }
293
294 /**
295 * Perform action depending on what mode we're in.
296 */
297 @Override public void mouseDragged(MouseEvent e) {
298 if(!Main.map.mapView.isActiveLayerVisible())
299 return;
300
301 // do not count anything as a drag if it lasts less than 100 milliseconds.
302 if (System.currentTimeMillis() - mouseDownTime < initialMoveDelay)
303 return;
304
305 if (mode == Mode.select) {
306 // Just sit tight and wait for mouse to be released.
307 } else {
308 //move, create new and extrude mode - move the selected segment
309
310 EastNorth initialMouseEn = Main.map.mapView.getEastNorth(initialMousePos.x, initialMousePos.y);
311 EastNorth mouseEn = Main.map.mapView.getEastNorth(e.getPoint().x, e.getPoint().y);
312 EastNorth mouseMovement = new EastNorth(mouseEn.getX() - initialMouseEn.getX(), mouseEn.getY() - initialMouseEn.getY());
313
314 double bestDistance = Double.POSITIVE_INFINITY;
315 EastNorth bestMovement = null;
316 activeMoveDirection = null;
317
318 //find the best movement direction and vector
319 for (ReferenceSegment direction : possibleMoveDirections) {
320 EastNorth movement = calculateSegmentOffset(initialN1en, initialN2en, direction.en, mouseEn);
321 if (movement == null) {
322 //if direction parallel to segment.
323 continue;
324 }
325
326 double distanceFromMouseMovement = movement.distance(mouseMovement);
327 if (bestDistance > distanceFromMouseMovement) {
328 bestDistance = distanceFromMouseMovement;
329 activeMoveDirection = direction;
330 bestMovement = movement;
331 }
332 }
333
334 newN1en = new EastNorth(initialN1en.getX() + bestMovement.getX(), initialN1en.getY() + bestMovement.getY());
335 newN2en = new EastNorth(initialN2en.getX() + bestMovement.getX(), initialN2en.getY() + bestMovement.getY());
336
337 // find out the movement distance, in metres
338 double distance = Main.getProjection().eastNorth2latlon(initialN1en).greatCircleDistance(Main.getProjection().eastNorth2latlon(newN1en));
339 Main.map.statusLine.setDist(distance);
340 updateStatusLine();
341
342 Main.map.mapView.setNewCursor(Cursor.MOVE_CURSOR, this);
343
344 if (mode == Mode.extrude || mode == Mode.create_new) {
345 //nothing here
346 } else if (mode == Mode.translate) {
347 //move nodes to new position
348 if (moveCommand == null) {
349 //make a new move command
350 Collection<OsmPrimitive> nodelist = new LinkedList<OsmPrimitive>();
351 nodelist.add(selectedSegment.getFirstNode());
352 nodelist.add(selectedSegment.getSecondNode());
353 moveCommand = new MoveCommand(nodelist, bestMovement.getX(), bestMovement.getY());
354 Main.main.undoRedo.add(moveCommand);
355 } else {
356 //reuse existing move command
357 moveCommand.moveAgainTo(bestMovement.getX(), bestMovement.getY());
358 }
359 }
360
361 Main.map.mapView.repaint();
362 }
363 }
364
365 /**
366 * Do anything that needs to be done, then switch back to select mode
367 */
368 @Override public void mouseReleased(MouseEvent e) {
369
370 if(!Main.map.mapView.isActiveLayerVisible())
371 return;
372
373 if (mode == Mode.select) {
374 // Nothing to be done
375 } else {
376 if (mode == Mode.create_new) {
377 if (e.getPoint().distance(initialMousePos) > 10 && newN1en != null) {
378 // crete a new rectangle
379 Collection<Command> cmds = new LinkedList<Command>();
380 Node third = new Node(newN2en);
381 Node fourth = new Node(newN1en);
382 Way wnew = new Way();
383 wnew.addNode(selectedSegment.getFirstNode());
384 wnew.addNode(selectedSegment.getSecondNode());
385 wnew.addNode(third);
386 wnew.addNode(fourth);
387 // ... and close the way
388 wnew.addNode(selectedSegment.getFirstNode());
389 // undo support
390 cmds.add(new AddCommand(third));
391 cmds.add(new AddCommand(fourth));
392 cmds.add(new AddCommand(wnew));
393 Command c = new SequenceCommand(tr("Extrude Way"), cmds);
394 Main.main.undoRedo.add(c);
395 getCurrentDataSet().setSelected(wnew);
396 }
397 } else if (mode == Mode.extrude) {
398 if( e.getClickCount() == 2 && e.getPoint().equals(initialMousePos) ) {
399 // double click add a new node
400 // Should maybe do the same as in DrawAction and fetch all nearby segments?
401 WaySegment ws = Main.map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive.isSelectablePredicate);
402 if (ws != null) {
403 Node n = new Node(Main.map.mapView.getLatLon(e.getX(), e.getY()));
404 EastNorth A = ws.getFirstNode().getEastNorth();
405 EastNorth B = ws.getSecondNode().getEastNorth();
406 n.setEastNorth(Geometry.closestPointToSegment(A, B, n.getEastNorth()));
407 Way wnew = new Way(ws.way);
408 wnew.addNode(ws.lowerIndex+1, n);
409 SequenceCommand cmds = new SequenceCommand(tr("Add a new node to an existing way"),
410 new AddCommand(n), new ChangeCommand(ws.way, wnew));
411 Main.main.undoRedo.add(cmds);
412 }
413 }
414 else if (e.getPoint().distance(initialMousePos) > 10 && newN1en != null && selectedSegment != null) {
415 // create extrusion
416
417 Collection<Command> cmds = new LinkedList<Command>();
418 Way wnew = new Way(selectedSegment.way);
419 int insertionPoint = selectedSegment.lowerIndex + 1;
420
421 //find if the new points overlap existing segments (in case of 90 degree angles)
422 Node prevNode = getPreviousNode(selectedSegment.lowerIndex);
423 boolean nodeOverlapsSegment = prevNode != null && Geometry.segmentsParallel(initialN1en, prevNode.getEastNorth(), initialN1en, newN1en);
424 boolean hasOtherWays = this.hasNodeOtherWays(selectedSegment.getFirstNode(), selectedSegment.way);
425
426 if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) {
427 //move existing node
428 Node n1Old = selectedSegment.getFirstNode();
429 cmds.add(new MoveCommand(n1Old, Main.getProjection().eastNorth2latlon(newN1en)));
430 } else {
431 //introduce new node
432 Node n1New = new Node(Main.getProjection().eastNorth2latlon(newN1en));
433 wnew.addNode(insertionPoint, n1New);
434 insertionPoint ++;
435 cmds.add(new AddCommand(n1New));
436 }
437
438 //find if the new points overlap existing segments (in case of 90 degree angles)
439 Node nextNode = getNextNode(selectedSegment.lowerIndex + 1);
440 nodeOverlapsSegment = nextNode != null && Geometry.segmentsParallel(initialN2en, nextNode.getEastNorth(), initialN2en, newN2en);
441 hasOtherWays = hasNodeOtherWays(selectedSegment.getSecondNode(), selectedSegment.way);
442
443 if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) {
444 //move existing node
445 Node n2Old = selectedSegment.getSecondNode();
446 cmds.add(new MoveCommand(n2Old, Main.getProjection().eastNorth2latlon(newN2en)));
447 } else {
448 //introduce new node
449 Node n2New = new Node(Main.getProjection().eastNorth2latlon(newN2en));
450 wnew.addNode(insertionPoint, n2New);
451 insertionPoint ++;
452 cmds.add(new AddCommand(n2New));
453 }
454
455 //the way was a single segment, close the way
456 if (wnew.getNodesCount() == 4) {
457 wnew.addNode(selectedSegment.getFirstNode());
458 }
459
460 cmds.add(new ChangeCommand(selectedSegment.way, wnew));
461 Command c = new SequenceCommand(tr("Extrude Way"), cmds);
462 Main.main.undoRedo.add(c);
463 }
464 } else if (mode == Mode.translate) {
465 //Commit translate
466 //the move command is already committed in mouseDragged
467 }
468
469 boolean alt = (e.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0;
470 boolean ctrl = (e.getModifiers() & (ActionEvent.CTRL_MASK)) != 0;
471 boolean shift = (e.getModifiers() & (ActionEvent.SHIFT_MASK)) != 0;
472 // Switch back into select mode
473 Main.map.mapView.setNewCursor(ctrl ? cursorTranslate : alt ? cursorCreateNew : shift ? cursorCreateNodes : cursor, this);
474 Main.map.mapView.removeTemporaryLayer(this);
475 selectedSegment = null;
476 moveCommand = null;
477 mode = Mode.select;
478
479 updateStatusLine();
480 Main.map.mapView.repaint();
481 }
482 }
483
484 /**
485 * This method tests if a node has other ways apart from the given one.
486 * @param node
487 * @param myWay
488 * @return true of node belongs only to myWay, false if there are more ways.
489 */
490 private boolean hasNodeOtherWays(Node node, Way myWay) {
491 for (OsmPrimitive p : node.getReferrers()) {
492 if (p instanceof Way && p.isUsable() && p != myWay)
493 return true;
494 }
495 return false;
496 }
497
498 /***
499 * This method calculates offset amount by witch to move the given segment perpendicularly for it to be in line with mouse position.
500 * @param segmentP1
501 * @param segmentP2
502 * @param targetPos
503 * @return offset amount of P1 and P2.
504 */
505 private static EastNorth calculateSegmentOffset(EastNorth segmentP1, EastNorth segmentP2, EastNorth moveDirection,
506 EastNorth targetPos) {
507 EastNorth intersectionPoint = Geometry.getLineLineIntersection(segmentP1, segmentP2, targetPos,
508 new EastNorth(targetPos.getX() + moveDirection.getX(), targetPos.getY() + moveDirection.getY()));
509
510 if (intersectionPoint == null)
511 return null;
512 else
513 //return distance form base to target position
514 return new EastNorth(targetPos.getX() - intersectionPoint.getX(),
515 targetPos.getY() - intersectionPoint.getY());
516 }
517
518 /**
519 * Gets a node from selected way before given index.
520 * @param index index of current node
521 * @return index of previous node or -1 if there are no nodes there.
522 */
523 private int getPreviousNodeIndex(int index) {
524 if (index > 0)
525 return index - 1;
526 else if (selectedSegment.way.isClosed())
527 return selectedSegment.way.getNodesCount() - 2;
528 else
529 return -1;
530 }
531
532 /**
533 * Gets a node from selected way before given index.
534 * @param index index of current node
535 * @return previous node or null if there are no nodes there.
536 */
537 private Node getPreviousNode(int index) {
538 int indexPrev = getPreviousNodeIndex(index);
539 if (indexPrev >= 0)
540 return selectedSegment.way.getNode(indexPrev);
541 else
542 return null;
543 }
544
545
546 /**
547 * Gets a node from selected way after given index.
548 * @param index index of current node
549 * @return index of next node or -1 if there are no nodes there.
550 */
551 private int getNextNodeIndex(int index) {
552 int count = selectedSegment.way.getNodesCount();
553 if (index < count - 1)
554 return index + 1;
555 else if (selectedSegment.way.isClosed())
556 return 1;
557 else
558 return -1;
559 }
560
561 /**
562 * Gets a node from selected way after given index.
563 * @param index index of current node
564 * @return next node or null if there are no nodes there.
565 */
566 private Node getNextNode(int index) {
567 int indexNext = getNextNodeIndex(index);
568 if (indexNext >= 0)
569 return selectedSegment.way.getNode(indexNext);
570 else
571 return null;
572 }
573
574 public void paint(Graphics2D g, MapView mv, Bounds box) {
575 if (mode == Mode.select) {
576 // Nothing to do
577 } else {
578 if (newN1en != null) {
579 Graphics2D g2 = g;
580 g2.setColor(selectedColor);
581 g2.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
582
583 Point p1 = mv.getPoint(initialN1en);
584 Point p2 = mv.getPoint(initialN2en);
585 Point p3 = mv.getPoint(newN1en);
586 Point p4 = mv.getPoint(newN2en);
587
588 double fac = 1.0 / activeMoveDirection.en.distance(0,0);
589 // mult by factor to get unit vector.
590 EastNorth normalUnitVector = new EastNorth(activeMoveDirection.en.getX() * fac, activeMoveDirection.en.getY() * fac);
591
592 // Check to see if our new N1 is in a positive direction with respect to the normalUnitVector.
593 // Even if the x component is zero, we should still be able to discern using +0.0 and -0.0
594 if (newN1en != null && ((newN1en.getX() > initialN1en.getX()) != (normalUnitVector.getX() > -0.0))) {
595 // If not, use a sign-flipped version of the normalUnitVector.
596 normalUnitVector = new EastNorth(-normalUnitVector.getX(), -normalUnitVector.getY());
597 }
598
599 //HACK: swap Y, because the target pixels are top down, but EastNorth is bottom-up.
600 //This is normally done by MapView.getPoint, but it does not work on vectors.
601 normalUnitVector.setLocation(normalUnitVector.getX(), -normalUnitVector.getY());
602
603 if (mode == Mode.extrude || mode == Mode.create_new) {
604 // Draw rectangle around new area.
605 GeneralPath b = new GeneralPath();
606 b.moveTo(p1.x, p1.y); b.lineTo(p3.x, p3.y);
607 b.lineTo(p4.x, p4.y); b.lineTo(p2.x, p2.y);
608 b.lineTo(p1.x, p1.y);
609 g2.draw(b);
610
611 if (activeMoveDirection != null) {
612 // Draw reference way
613 Point pr1 = mv.getPoint(activeMoveDirection.ws.getFirstNode().getEastNorth());
614 Point pr2 = mv.getPoint(activeMoveDirection.ws.getSecondNode().getEastNorth());
615 b = new GeneralPath();
616 b.moveTo(pr1.x, pr1.y);
617 b.lineTo(pr2.x, pr2.y);
618 g2.setColor(helperColor);
619 g2.setStroke(helperStrokeDash);
620 g2.draw(b);
621
622 // Draw right angle marker on first node position, only when moving at right angle
623 if (activeMoveDirection.perpendicular) {
624 // mirror RightAngle marker, so it is inside the extrude
625 double headingRefWS = activeMoveDirection.ws.getFirstNode().getEastNorth().heading(activeMoveDirection.ws.getSecondNode().getEastNorth());
626 double headingMoveDir = Math.atan2(normalUnitVector.getY(), normalUnitVector.getX());
627 double headingDiff = headingRefWS - headingMoveDir;
628 if (headingDiff < 0) headingDiff += 2 * Math.PI;
629 boolean mirrorRA = Math.abs(headingDiff - Math.PI) > 1e-5;
630
631 // EastNorth units per pixel
632 double factor = 1.0/g2.getTransform().getScaleX();
633 double raoffsetx = 8.0*factor*normalUnitVector.getX();
634 double raoffsety = 8.0*factor*normalUnitVector.getY();
635
636 Point2D ra1 = new Point2D.Double(pr1.x + raoffsetx, pr1.y+raoffsety);
637 Point2D ra3 = new Point2D.Double(pr1.x - raoffsety*(mirrorRA ? -1 : 1), pr1.y + raoffsetx*(mirrorRA ? -1 : 1));
638 Point2D ra2 = new Point2D.Double(ra1.getX() - raoffsety*(mirrorRA ? -1 : 1), ra1.getY() + raoffsetx*(mirrorRA ? -1 : 1));
639 GeneralPath ra = new GeneralPath();
640 ra.moveTo((float)ra1.getX(), (float)ra1.getY());
641 ra.lineTo((float)ra2.getX(), (float)ra2.getY());
642 ra.lineTo((float)ra3.getX(), (float)ra3.getY());
643 g2.setStroke(helperStrokeRA);
644 g2.draw(ra);
645 }
646 }
647 } else if (mode == Mode.translate) {
648 // Highlight the new and old segments.
649 Line2D newline = new Line2D.Double(p3, p4);
650 g2.draw(newline);
651 g2.setStroke(new BasicStroke(1));
652 Line2D oldline = new Line2D.Double(p1, p2);
653 g2.draw(oldline);
654
655 if (activeMoveDirection != null) {
656
657 // Draw a guideline along the normal.
658 Line2D normline;
659 Point2D centerpoint = new Point2D.Double((p1.getX()+p2.getX())*0.5, (p1.getY()+p2.getY())*0.5);
660 normline = createSemiInfiniteLine(centerpoint, normalUnitVector, g2);
661 g2.draw(normline);
662
663 // Draw right angle marker on initial position, only when moving at right angle
664 if (activeMoveDirection.perpendicular) {
665 // EastNorth units per pixel
666 double factor = 1.0/g2.getTransform().getScaleX();
667
668 double raoffsetx = 8.0*factor*normalUnitVector.getX();
669 double raoffsety = 8.0*factor*normalUnitVector.getY();
670 Point2D ra1 = new Point2D.Double(centerpoint.getX()+raoffsetx, centerpoint.getY()+raoffsety);
671 Point2D ra3 = new Point2D.Double(centerpoint.getX()-raoffsety, centerpoint.getY()+raoffsetx);
672 Point2D ra2 = new Point2D.Double(ra1.getX()-raoffsety, ra1.getY()+raoffsetx);
673 GeneralPath ra = new GeneralPath();
674 ra.moveTo((float)ra1.getX(), (float)ra1.getY());
675 ra.lineTo((float)ra2.getX(), (float)ra2.getY());
676 ra.lineTo((float)ra3.getX(), (float)ra3.getY());
677 g2.draw(ra);
678 }
679 }
680 }
681 }
682 }
683 }
684
685 /**
686 * Create a new Line that extends off the edge of the viewport in one direction
687 * @param start The start point of the line
688 * @param unitvector A unit vector denoting the direction of the line
689 * @param g the Graphics2D object it will be used on
690 */
691 static private Line2D createSemiInfiniteLine(Point2D start, Point2D unitvector, Graphics2D g) {
692 Rectangle bounds = g.getDeviceConfiguration().getBounds();
693 try {
694 AffineTransform invtrans = g.getTransform().createInverse();
695 Point2D widthpoint = invtrans.deltaTransform(new Point2D.Double(bounds.width,0), null);
696 Point2D heightpoint = invtrans.deltaTransform(new Point2D.Double(0,bounds.height), null);
697
698 // Here we should end up with a gross overestimate of the maximum viewport diagonal in what
699 // Graphics2D calls 'user space'. Essentially a manhattan distance of manhattan distances.
700 // This can be used as a safe length of line to generate which will always go off-viewport.
701 double linelength = Math.abs(widthpoint.getX()) + Math.abs(widthpoint.getY()) + Math.abs(heightpoint.getX()) + Math.abs(heightpoint.getY());
702
703 return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * linelength) , start.getY() + (unitvector.getY() * linelength)));
704 }
705 catch (NoninvertibleTransformException e) {
706 return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * 10) , start.getY() + (unitvector.getY() * 10)));
707 }
708 }
709 }