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

Last change on this file since 3631 was 3631, checked in by bastiK, 14 years ago

see #5560 (patch by extropy) - Extrude does not work with multiple adjacent buildings

  • Property svn:eol-style set to native
File size: 24.2 KB
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.actions.mapmode;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.BasicStroke;
7import java.awt.Color;
8import java.awt.Cursor;
9import java.awt.Graphics2D;
10import java.awt.Point;
11import java.awt.Rectangle;
12import java.awt.event.ActionEvent;
13import java.awt.event.KeyEvent;
14import java.awt.event.MouseEvent;
15import java.awt.geom.AffineTransform;
16import java.awt.geom.GeneralPath;
17import java.awt.geom.Line2D;
18import java.awt.geom.NoninvertibleTransformException;
19import java.awt.geom.Point2D;
20import java.util.ArrayList;
21import java.util.Collection;
22import java.util.LinkedList;
23import java.util.List;
24
25import org.openstreetmap.josm.Main;
26import org.openstreetmap.josm.command.AddCommand;
27import org.openstreetmap.josm.command.ChangeCommand;
28import org.openstreetmap.josm.command.Command;
29import org.openstreetmap.josm.command.MoveCommand;
30import org.openstreetmap.josm.command.SequenceCommand;
31import org.openstreetmap.josm.data.Bounds;
32import org.openstreetmap.josm.data.coor.EastNorth;
33import org.openstreetmap.josm.data.osm.Node;
34import org.openstreetmap.josm.data.osm.OsmPrimitive;
35import org.openstreetmap.josm.data.osm.Way;
36import org.openstreetmap.josm.data.osm.WaySegment;
37import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
38import org.openstreetmap.josm.gui.MapFrame;
39import org.openstreetmap.josm.gui.MapView;
40import org.openstreetmap.josm.gui.layer.Layer;
41import org.openstreetmap.josm.gui.layer.MapViewPaintable;
42import org.openstreetmap.josm.gui.layer.OsmDataLayer;
43import org.openstreetmap.josm.tools.ImageProvider;
44import org.openstreetmap.josm.tools.Shortcut;
45
46/**
47 * Makes a rectangle from a line, or modifies a rectangle.
48 */
49public class ExtrudeAction extends MapMode implements MapViewPaintable {
50
51 enum Mode { extrude, translate, select }
52
53 private Mode mode = Mode.select;
54
55 /**
56 * If true, when extruding create new node even if segments parallel.
57 */
58 private boolean alwaysCreateNodes = false;
59 private long mouseDownTime = 0;
60 private WaySegment selectedSegment = null;
61 private Color selectedColor;
62
63 /**
64 * Possible directions to move to.
65 */
66 private List<EastNorth> possibleMoveDirections;
67
68 /**
69 * The direction that is currently active.
70 */
71 private EastNorth activeMoveDirection;
72
73 /**
74 * The old cursor before the user pressed the mouse button.
75 */
76 private Cursor oldCursor;
77 /**
78 * The position of the mouse cursor when the drag action was initiated.
79 */
80 private Point initialMousePos;
81 /**
82 * The time which needs to pass between click and release before something
83 * counts as a move, in milliseconds
84 */
85 private int initialMoveDelay = 200;
86 /**
87 * The initial EastNorths of node1 and node2
88 */
89 private EastNorth initialN1en;
90 private EastNorth initialN2en;
91 /**
92 * The new EastNorths of node1 and node2
93 */
94 private EastNorth newN1en;
95 private EastNorth newN2en;
96
97 /**
98 * the command that performed last move.
99 */
100 private MoveCommand moveCommand;
101
102 /**
103 * Create a new SelectAction
104 * @param mapFrame The MapFrame this action belongs to.
105 */
106 public ExtrudeAction(MapFrame mapFrame) {
107 super(tr("Extrude"), "extrude/extrude", tr("Create areas"),
108 Shortcut.registerShortcut("mapmode:extrude", tr("Mode: {0}", tr("Extrude")), KeyEvent.VK_X, Shortcut.GROUP_EDIT),
109 mapFrame,
110 getCursor("normal", "rectangle", Cursor.DEFAULT_CURSOR));
111 putValue("help", "Action/Extrude/Extrude");
112 initialMoveDelay = Main.pref.getInteger("edit.initial-move-delay",200);
113 selectedColor = PaintColors.SELECTED.get();
114 }
115
116 @Override public String getModeHelpText() {
117 if (mode == Mode.translate)
118 return tr("Move a segment along its normal, then release the mouse button.");
119 else if (mode == Mode.extrude)
120 return tr("Draw a rectangle of the desired size, then release the mouse button.");
121 else
122 return tr("Drag a way segment to make a rectangle. Ctrl-drag to move a segment along its normal.");
123 }
124
125 @Override public boolean layerIsSupported(Layer l) {
126 return l instanceof OsmDataLayer;
127 }
128
129 @Override public void enterMode() {
130 super.enterMode();
131 Main.map.mapView.addMouseListener(this);
132 Main.map.mapView.addMouseMotionListener(this);
133 }
134
135 @Override public void exitMode() {
136 Main.map.mapView.removeMouseListener(this);
137 Main.map.mapView.removeMouseMotionListener(this);
138 Main.map.mapView.removeTemporaryLayer(this);
139 super.exitMode();
140 }
141
142 /**
143 * If the left mouse button is pressed over a segment, switch
144 * to either extrude or translate mode depending on whether Ctrl is held.
145 */
146 @Override public void mousePressed(MouseEvent e) {
147 if(!Main.map.mapView.isActiveLayerVisible())
148 return;
149 if (!(Boolean)this.getValue("active"))
150 return;
151 if (e.getButton() != MouseEvent.BUTTON1)
152 return;
153
154 selectedSegment = Main.map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive.isSelectablePredicate);
155
156 if (selectedSegment == null) {
157 // If nothing gets caught, stay in select mode
158 } else {
159 // Otherwise switch to another mode
160
161 if ((e.getModifiers() & ActionEvent.CTRL_MASK) != 0) {
162 mode = Mode.translate;
163 } else {
164 mode = Mode.extrude;
165 getCurrentDataSet().setSelected(selectedSegment.way);
166 alwaysCreateNodes = ((e.getModifiers() & ActionEvent.SHIFT_MASK) != 0);
167 }
168
169 // remember initial positions for segment nodes.
170 initialN1en = selectedSegment.getFirstNode().getEastNorth();
171 initialN2en = selectedSegment.getSecondNode().getEastNorth();
172
173 //gather possible move directions - perpendicular to the selected segment and parallel to neighbor segments
174 possibleMoveDirections = new ArrayList<EastNorth>();
175 possibleMoveDirections.add(new EastNorth(
176 initialN1en.getY() - initialN2en.getY(),
177 initialN2en.getX() - initialN1en.getX()));
178
179 //add directions parallel to neighbor segments
180
181 Node prevNode = getPreviousNode(selectedSegment.lowerIndex);
182 if (prevNode != null) {
183 EastNorth en = prevNode.getEastNorth();
184 possibleMoveDirections.add(new EastNorth(
185 initialN1en.getX() - en.getX(),
186 initialN1en.getY() - en.getY()));
187 }
188
189 Node nextNode = getNextNode(selectedSegment.lowerIndex + 1);
190 if (nextNode != null) {
191 EastNorth en = nextNode.getEastNorth();
192 possibleMoveDirections.add(new EastNorth(
193 initialN2en.getX() - en.getX(),
194 initialN2en.getY() - en.getY()));
195 }
196
197 // Signifies that nothing has happened yet
198 newN1en = null;
199 newN2en = null;
200 moveCommand = null;
201
202 Main.map.mapView.addTemporaryLayer(this);
203
204 updateStatusLine();
205 Main.map.mapView.repaint();
206
207 // Make note of time pressed
208 mouseDownTime = System.currentTimeMillis();
209
210 // Make note of mouse position
211 initialMousePos = e.getPoint();
212 }
213 }
214
215 /**
216 * Perform action depending on what mode we're in.
217 */
218 @Override public void mouseDragged(MouseEvent e) {
219 if(!Main.map.mapView.isActiveLayerVisible())
220 return;
221
222 // do not count anything as a drag if it lasts less than 100 milliseconds.
223 if (System.currentTimeMillis() - mouseDownTime < initialMoveDelay)
224 return;
225
226 if (mode == Mode.select) {
227 // Just sit tight and wait for mouse to be released.
228 } else {
229 //move and extrude mode - move the selected segment
230
231 EastNorth initialMouseEn = Main.map.mapView.getEastNorth(initialMousePos.x, initialMousePos.y);
232 EastNorth mouseEn = Main.map.mapView.getEastNorth(e.getPoint().x, e.getPoint().y);
233 EastNorth mouseMovement = new EastNorth(mouseEn.getX() - initialMouseEn.getX(), mouseEn.getY() - initialMouseEn.getY());
234
235 double bestDistance = Double.POSITIVE_INFINITY;
236 EastNorth bestMovement = null;
237 activeMoveDirection = null;
238
239 //find the best movement direction and vector
240 for (EastNorth direction: possibleMoveDirections) {
241 EastNorth movement = calculateSegmentOffset(initialN1en, initialN2en, direction , mouseEn);
242 if (movement == null) {
243 //if direction parallel to segment.
244 continue;
245 }
246
247 double distanceFromMouseMovement = movement.distance(mouseMovement);
248 if (bestDistance > distanceFromMouseMovement) {
249 bestDistance = distanceFromMouseMovement;
250 activeMoveDirection = direction;
251 bestMovement = movement;
252 }
253 }
254
255 newN1en = new EastNorth(initialN1en.getX() + bestMovement.getX(), initialN1en.getY() + bestMovement.getY());
256 newN2en = new EastNorth(initialN2en.getX() + bestMovement.getX(), initialN2en.getY() + bestMovement.getY());
257
258 // find out the movement distance, in metres
259 double distance = Main.proj.eastNorth2latlon(initialN1en).greatCircleDistance(Main.proj.eastNorth2latlon(newN1en));
260 Main.map.statusLine.setDist(distance);
261 updateStatusLine();
262
263 setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
264
265 if (mode == Mode.extrude) {
266 //nothing here
267 } else if (mode == Mode.translate) {
268 //move nodes to new position
269 if (moveCommand == null) {
270 //make a new move command
271 Collection<OsmPrimitive> nodelist = new LinkedList<OsmPrimitive>();
272 nodelist.add(selectedSegment.getFirstNode());
273 nodelist.add(selectedSegment.getSecondNode());
274 moveCommand = new MoveCommand(nodelist, bestMovement.getX(), bestMovement.getY());
275 Main.main.undoRedo.add(moveCommand);
276 } else {
277 //reuse existing move command
278 moveCommand.undoCommand();
279 moveCommand.moveAgain(bestMovement.getX(), bestMovement.getY());
280 }
281 }
282
283 Main.map.mapView.repaint();
284 }
285 }
286
287 /**
288 * Do anything that needs to be done, then switch back to select mode
289 */
290 @Override public void mouseReleased(MouseEvent e) {
291
292 if(!Main.map.mapView.isActiveLayerVisible())
293 return;
294
295 if (mode == Mode.select) {
296 // Nothing to be done
297 } else {
298 if (mode == Mode.extrude) {
299 if (e.getPoint().distance(initialMousePos) > 10 && newN1en != null) {
300 // create extrusion
301
302 Collection<Command> cmds = new LinkedList<Command>();
303 Way wnew = new Way(selectedSegment.way);
304 int insertionPoint = selectedSegment.lowerIndex + 1;
305
306 //find if the new points overlap existing segments (in case of 90 degree angles)
307 Node prevNode = getPreviousNode(selectedSegment.lowerIndex);
308 boolean nodeOverlapsSegment = prevNode != null && segmentsParralel(initialN1en, prevNode.getEastNorth(), initialN1en, newN1en);
309 boolean hasOtherWays = this.hasNodeOtherWays(selectedSegment.getFirstNode(), selectedSegment.way);
310
311 if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) {
312 //move existing node
313 Node n1Old = selectedSegment.getFirstNode();
314 cmds.add(new MoveCommand(n1Old, Main.proj.eastNorth2latlon(newN1en)));
315 } else {
316 //introduce new node
317 Node n1New = new Node(Main.proj.eastNorth2latlon(newN1en));
318 wnew.addNode(insertionPoint, n1New);
319 insertionPoint ++;
320 cmds.add(new AddCommand(n1New));
321 }
322
323 //find if the new points overlap existing segments (in case of 90 degree angles)
324 Node nextNode = getNextNode(selectedSegment.lowerIndex + 1);
325 nodeOverlapsSegment = nextNode != null && segmentsParralel(initialN2en, nextNode.getEastNorth(), initialN2en, newN2en);
326 hasOtherWays = hasNodeOtherWays(selectedSegment.getFirstNode(), selectedSegment.way);
327
328 if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) {
329 //move existing node
330 Node n2Old = selectedSegment.getSecondNode();
331 cmds.add(new MoveCommand(n2Old, Main.proj.eastNorth2latlon(newN2en)));
332 } else {
333 //introduce new node
334 Node n2New = new Node(Main.proj.eastNorth2latlon(newN2en));
335 wnew.addNode(insertionPoint, n2New);
336 insertionPoint ++;
337 cmds.add(new AddCommand(n2New));
338 }
339
340 //the way was a single segment, close the way
341 if (wnew.getNodesCount() == 4) {
342 wnew.addNode(selectedSegment.getFirstNode());
343 }
344
345 cmds.add(new ChangeCommand(selectedSegment.way, wnew));
346 Command c = new SequenceCommand(tr("Extrude Way"), cmds);
347 Main.main.undoRedo.add(c);
348 }
349 } else if (mode == Mode.translate) {
350 //Commit translate
351 //the move command is already committed in mouseDragged
352 }
353
354 // Switch back into select mode
355 restoreCursor();
356 Main.map.mapView.removeTemporaryLayer(this);
357 selectedSegment = null;
358 moveCommand = null;
359 mode = Mode.select;
360
361 updateStatusLine();
362 Main.map.mapView.repaint();
363 }
364 }
365
366 /**
367 * This method tests if a node has other ways apart from the given one.
368 * @param node
369 * @param myWay
370 * @return true of node belongs only to myWay, false if there are more ways.
371 */
372 private boolean hasNodeOtherWays(Node node, Way myWay) {
373 for (OsmPrimitive p : node.getReferrers()) {
374 if (p instanceof Way && p.isUsable() && p != myWay)
375 return true;
376 }
377 return false;
378 }
379
380 /***
381 * This method calculates offset amount by witch to move the given segment perpendicularly for it to be in line with mouse position.
382 * @param segmentP1
383 * @param segmentP2
384 * @param targetPos
385 * @return offset amount of P1 and P2.
386 */
387 private static EastNorth calculateSegmentOffset(EastNorth segmentP1, EastNorth segmentP2, EastNorth moveDirection,
388 EastNorth targetPos) {
389 EastNorth intersectionPoint = getLineLineIntersection(segmentP1, segmentP2, targetPos,
390 new EastNorth(targetPos.getX() + moveDirection.getX(), targetPos.getY() + moveDirection.getY()));
391
392 if (intersectionPoint == null)
393 return null;
394 else
395 //return distance form base to target position
396 return new EastNorth(targetPos.getX() - intersectionPoint.getX(),
397 targetPos.getY() - intersectionPoint.getY());
398 }
399
400 /**
401 * Finds the intersection of two lines of infinite length.
402 * @return EastNorth null if no intersection was found, the coordinates of the intersection otherwise
403 */
404 public static EastNorth getLineLineIntersection(EastNorth p1, EastNorth p2, EastNorth p3, EastNorth p4) {
405 // Convert line from (point, point) form to ax+by=c
406 double a1 = p2.getY() - p1.getY();
407 double b1 = p1.getX() - p2.getX();
408 double c1 = p2.getX() * p1.getY() - p1.getX() * p2.getY();
409
410 double a2 = p4.getY() - p3.getY();
411 double b2 = p3.getX() - p4.getX();
412 double c2 = p4.getX() * p3.getY() - p3.getX() * p4.getY();
413
414 // Solve the equations
415 double det = a1 * b2 - a2 * b1;
416 if (det == 0)
417 return null; // Lines are parallel
418
419 return new EastNorth((b1 * c2 - b2 * c1) / det, (a2 * c1 - a1 * c2) / det);
420 }
421
422 private static boolean segmentsParralel(EastNorth p1, EastNorth p2, EastNorth p3, EastNorth p4) {
423
424 // Convert line from (point, point) form to ax+by=c
425 double a1 = p2.getY() - p1.getY();
426 double b1 = p1.getX() - p2.getX();
427
428 double a2 = p4.getY() - p3.getY();
429 double b2 = p3.getX() - p4.getX();
430
431 // Solve the equations
432 double det = a1 * b2 - a2 * b1;
433 return Math.abs(det) < 1e-13;
434 }
435
436 /**
437 * Gets a node from selected way before given index.
438 * @param index index of current node
439 * @return previous node or null if there are no nodes there.
440 */
441 private Node getPreviousNode(int index) {
442 if (index > 0)
443 return selectedSegment.way.getNode(index - 1);
444 else if (selectedSegment.way.isClosed())
445 return selectedSegment.way.getNode(selectedSegment.way.getNodesCount() - 2);
446 else
447 return null;
448 }
449
450 /**
451 * Gets a node from selected way before given index.
452 * @param index index of current node
453 * @return next node or null if there are no nodes there.
454 */
455 private Node getNextNode(int index) {
456 int count = selectedSegment.way.getNodesCount();
457 if (index < count - 1)
458 return selectedSegment.way.getNode(index + 1);
459 else if (selectedSegment.way.isClosed())
460 return selectedSegment.way.getNode(1);
461 else
462 return null;
463 }
464
465 public void paint(Graphics2D g, MapView mv, Bounds box) {
466 if (mode == Mode.select) {
467 // Nothing to do
468 } else {
469 if (newN1en != null) {
470 Graphics2D g2 = g;
471 g2.setColor(selectedColor);
472 g2.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
473
474 Point p1 = mv.getPoint(initialN1en);
475 Point p2 = mv.getPoint(initialN2en);
476 Point p3 = mv.getPoint(newN1en);
477 Point p4 = mv.getPoint(newN2en);
478
479 if (mode == Mode.extrude) {
480 // Draw rectangle around new area.
481 GeneralPath b = new GeneralPath();
482 b.moveTo(p1.x, p1.y); b.lineTo(p3.x, p3.y);
483 b.lineTo(p4.x, p4.y); b.lineTo(p2.x, p2.y);
484 b.lineTo(p1.x, p1.y);
485 g2.draw(b);
486 g2.setStroke(new BasicStroke(1));
487 } else if (mode == Mode.translate) {
488 // Highlight the new and old segments.
489 Line2D newline = new Line2D.Double(p3, p4);
490 g2.draw(newline);
491 g2.setStroke(new BasicStroke(1));
492 Line2D oldline = new Line2D.Double(p1, p2);
493 g2.draw(oldline);
494
495 if (activeMoveDirection != null) {
496
497 double fac = 1.0 / activeMoveDirection.distance(0,0);
498 // mult by factor to get unit vector.
499 EastNorth normalUnitVector = new EastNorth(activeMoveDirection.getX() * fac, activeMoveDirection.getY() * fac);
500
501 // Check to see if our new N1 is in a positive direction with respect to the normalUnitVector.
502 // Even if the x component is zero, we should still be able to discern using +0.0 and -0.0
503 if (newN1en != null && (newN1en.getX() > initialN1en.getX() != normalUnitVector.getX() > -0.0)) {
504 // If not, use a sign-flipped version of the normalUnitVector.
505 normalUnitVector = new EastNorth(-normalUnitVector.getX(), -normalUnitVector.getY());
506 }
507
508 //HACK: swap Y, because the target pixels are top down, but EastNorth is bottom-up.
509 //This is normally done by MapView.getPoint, but it does not work on vectors.
510 normalUnitVector.setLocation(normalUnitVector.getX(), -normalUnitVector.getY());
511
512 // Draw a guideline along the normal.
513 Line2D normline;
514 Point2D centerpoint = new Point2D.Double((p1.getX()+p2.getX())*0.5, (p1.getY()+p2.getY())*0.5);
515 normline = createSemiInfiniteLine(centerpoint, normalUnitVector, g2);
516 g2.draw(normline);
517
518 // Draw right angle marker on initial position, only when moving at right angle
519 if (activeMoveDirection == possibleMoveDirections.get(0)) {
520 // EastNorth units per pixel
521 double factor = 1.0/g2.getTransform().getScaleX();
522
523 double raoffsetx = 8.0*factor*normalUnitVector.getX();
524 double raoffsety = 8.0*factor*normalUnitVector.getY();
525 Point2D ra1 = new Point2D.Double(centerpoint.getX()+raoffsetx, centerpoint.getY()+raoffsety);
526 Point2D ra3 = new Point2D.Double(centerpoint.getX()-raoffsety, centerpoint.getY()+raoffsetx);
527 Point2D ra2 = new Point2D.Double(ra1.getX()-raoffsety, ra1.getY()+raoffsetx);
528 GeneralPath ra = new GeneralPath();
529 ra.moveTo((float)ra1.getX(), (float)ra1.getY());
530 ra.lineTo((float)ra2.getX(), (float)ra2.getY());
531 ra.lineTo((float)ra3.getX(), (float)ra3.getY());
532 g2.draw(ra);
533 }
534 }
535 }
536 }
537 }
538 }
539
540 /**
541 * Create a new Line that extends off the edge of the viewport in one direction
542 * @param start The start point of the line
543 * @param unitvector A unit vector denoting the direction of the line
544 * @param g the Graphics2D object it will be used on
545 */
546 static private Line2D createSemiInfiniteLine(Point2D start, Point2D unitvector, Graphics2D g) {
547 Rectangle bounds = g.getDeviceConfiguration().getBounds();
548 try {
549 AffineTransform invtrans = g.getTransform().createInverse();
550 Point2D widthpoint = invtrans.deltaTransform(new Point2D.Double(bounds.width,0), null);
551 Point2D heightpoint = invtrans.deltaTransform(new Point2D.Double(0,bounds.height), null);
552
553 // Here we should end up with a gross overestimate of the maximum viewport diagonal in what
554 // Graphics2D calls 'user space'. Essentially a manhattan distance of manhattan distances.
555 // This can be used as a safe length of line to generate which will always go off-viewport.
556 double linelength = Math.abs(widthpoint.getX()) + Math.abs(widthpoint.getY()) + Math.abs(heightpoint.getX()) + Math.abs(heightpoint.getY());
557
558 return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * linelength) , start.getY() + (unitvector.getY() * linelength)));
559 }
560 catch (NoninvertibleTransformException e) {
561 return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * 10) , start.getY() + (unitvector.getY() * 10)));
562 }
563 }
564
565 private static Cursor getCursor(String name, String mod, int def) {
566 try {
567 return ImageProvider.getCursor(name, mod);
568 } catch (Exception e) {
569 }
570 return Cursor.getPredefinedCursor(def);
571 }
572
573 private void setCursor(Cursor c) {
574 if (oldCursor == null) {
575 oldCursor = Main.map.mapView.getCursor();
576 Main.map.mapView.setCursor(c);
577 }
578 }
579
580 private void restoreCursor() {
581 if (oldCursor != null) {
582 Main.map.mapView.setCursor(oldCursor);
583 oldCursor = null;
584 }
585 }
586}
Note: See TracBrowser for help on using the repository browser.