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

Last change on this file since 3919 was 3919, checked in by stoecker, 13 years ago

unify cursor handling, hopefully fix #5381

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