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

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

fixed #4256 - Extrude snaps to a sparse grid (Changed the geometrical calculations back to how they were before [2429])

  • Property svn:eol-style set to native
File size: 18.0 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.Collection;
21import java.util.LinkedList;
22
23import org.openstreetmap.josm.Main;
24import org.openstreetmap.josm.command.AddCommand;
25import org.openstreetmap.josm.command.ChangeCommand;
26import org.openstreetmap.josm.command.Command;
27import org.openstreetmap.josm.command.MoveCommand;
28import org.openstreetmap.josm.command.SequenceCommand;
29import org.openstreetmap.josm.data.Bounds;
30import org.openstreetmap.josm.data.coor.EastNorth;
31import org.openstreetmap.josm.data.osm.Node;
32import org.openstreetmap.josm.data.osm.OsmPrimitive;
33import org.openstreetmap.josm.data.osm.Way;
34import org.openstreetmap.josm.data.osm.WaySegment;
35import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
36import org.openstreetmap.josm.gui.MapFrame;
37import org.openstreetmap.josm.gui.MapView;
38import org.openstreetmap.josm.gui.layer.Layer;
39import org.openstreetmap.josm.gui.layer.MapViewPaintable;
40import org.openstreetmap.josm.gui.layer.OsmDataLayer;
41import org.openstreetmap.josm.tools.ImageProvider;
42import org.openstreetmap.josm.tools.Shortcut;
43
44/**
45 * Makes a rectangle from a line, or modifies a rectangle.
46 */
47public class ExtrudeAction extends MapMode implements MapViewPaintable {
48
49 enum Mode { extrude, translate, select }
50 private Mode mode = Mode.select;
51 private long mouseDownTime = 0;
52 private WaySegment selectedSegment = null;
53 private Color selectedColor;
54
55 /**
56 * The old cursor before the user pressed the mouse button.
57 */
58 private Cursor oldCursor;
59 /**
60 * The position of the mouse cursor when the drag action was initiated.
61 */
62 private Point initialMousePos;
63 /**
64 * The time which needs to pass between click and release before something
65 * counts as a move, in milliseconds
66 */
67 private int initialMoveDelay = 200;
68 /**
69 * The initial EastNorths of node1 and node2
70 */
71 private EastNorth initialN1en;
72 private EastNorth initialN2en;
73 /**
74 * The new EastNorths of node1 and node2
75 */
76 private EastNorth newN1en;
77 private EastNorth newN2en;
78 /**
79 * This is to work around some deficiencies in MoveCommand when translating
80 */
81 private EastNorth lastTranslatedN1en;
82 /**
83 * Create a new SelectAction
84 * @param mapFrame The MapFrame this action belongs to.
85 */
86 public ExtrudeAction(MapFrame mapFrame) {
87 super(tr("Extrude"), "extrude/extrude", tr("Create areas"),
88 Shortcut.registerShortcut("mapmode:extrude", tr("Mode: {0}", tr("Extrude")), KeyEvent.VK_X, Shortcut.GROUP_EDIT),
89 mapFrame,
90 getCursor("normal", "rectangle", Cursor.DEFAULT_CURSOR));
91 putValue("help", "Action/Extrude/Extrude");
92 initialMoveDelay = Main.pref.getInteger("edit.initial-move-delay",200);
93 selectedColor = PaintColors.SELECTED.get();
94 }
95
96 private static Cursor getCursor(String name, String mod, int def) {
97 try {
98 return ImageProvider.getCursor(name, mod);
99 } catch (Exception e) {
100 }
101 return Cursor.getPredefinedCursor(def);
102 }
103
104 private void setCursor(Cursor c) {
105 if (oldCursor == null) {
106 oldCursor = Main.map.mapView.getCursor();
107 Main.map.mapView.setCursor(c);
108 }
109 }
110
111 private void restoreCursor() {
112 if (oldCursor != null) {
113 Main.map.mapView.setCursor(oldCursor);
114 oldCursor = null;
115 }
116 }
117
118 @Override public void enterMode() {
119 super.enterMode();
120 Main.map.mapView.addMouseListener(this);
121 Main.map.mapView.addMouseMotionListener(this);
122 }
123
124 @Override public void exitMode() {
125 super.exitMode();
126 Main.map.mapView.removeMouseListener(this);
127 Main.map.mapView.removeMouseMotionListener(this);
128 Main.map.mapView.removeTemporaryLayer(this);
129 }
130
131 /**
132 * Perform action depending on what mode we're in.
133 */
134 @Override public void mouseDragged(MouseEvent e) {
135 if(!Main.map.mapView.isActiveLayerVisible())
136 return;
137
138 // do not count anything as a drag if it lasts less than 100 milliseconds.
139 if (System.currentTimeMillis() - mouseDownTime < initialMoveDelay) return;
140
141 if (mode == Mode.select) {
142 // Just sit tight and wait for mouse to be released.
143 } else {
144 Node nd1 = selectedSegment.way.getNode(selectedSegment.lowerIndex);
145 Node nd2 = selectedSegment.way.getNode(selectedSegment.lowerIndex + 1);
146
147 EastNorth en1 = nd1.getEastNorth();
148 EastNorth en2 = nd2.getEastNorth();
149 EastNorth en3 = Main.map.mapView.getEastNorth(e.getPoint().x, e.getPoint().y);
150
151 double u = ((en3.east() - en1.east()) * (en2.east() - en1.east()) +
152 (en3.north() - en1.north()) * (en2.north() - en1.north())) /
153 en2.distanceSq(en1);
154 // the point on the segment from which the distance to mouse pos is shortest
155 EastNorth base = new EastNorth(en1.east() + u * (en2.east() - en1.east()),
156 en1.north() + u * (en2.north() - en1.north()));
157
158 // find out the distance, in metres, between the base point and the mouse cursor
159 double distance = Main.proj.eastNorth2latlon(base).greatCircleDistance(Main.proj.eastNorth2latlon(en3));
160 Main.map.statusLine.setDist(distance);
161 updateStatusLine();
162
163 // compute vertical and horizontal components.
164 double xoff = en3.east() - base.east();
165 double yoff = en3.north() - base.north();
166
167 newN1en = new EastNorth(en1.getX() + xoff, en1.getY() + yoff);
168 newN2en = new EastNorth(en2.getX() + xoff, en2.getY() + yoff);
169
170 // find out the distance, in metres, between the initial position of N1 and the new one.
171 Main.map.statusLine.setDist(Main.proj.eastNorth2latlon(initialN1en).greatCircleDistance(Main.proj.eastNorth2latlon(newN1en)));
172 updateStatusLine();
173
174 setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
175
176 if (mode == Mode.extrude) {
177
178 } else if (mode == Mode.translate) {
179 Command c = !Main.main.undoRedo.commands.isEmpty()
180 ? Main.main.undoRedo.commands.getLast() : null;
181 if (c instanceof SequenceCommand) {
182 c = ((SequenceCommand)c).getLastCommand();
183 }
184
185 Node n1 = selectedSegment.way.getNode(selectedSegment.lowerIndex);
186 Node n2 = selectedSegment.way.getNode(selectedSegment.lowerIndex+1);
187
188 EastNorth difference = new EastNorth(newN1en.getX()-lastTranslatedN1en.getX(), newN1en.getY()-lastTranslatedN1en.getY());
189
190 // Better way of testing list equality non-order-sensitively?
191 if (c instanceof MoveCommand
192 && ((MoveCommand)c).getMovedNodes().contains(n1)
193 && ((MoveCommand)c).getMovedNodes().contains(n2)
194 && ((MoveCommand)c).getMovedNodes().size() == 2) {
195 // MoveCommand doesn't let us know how much it has already moved the selection
196 // so we have to do some ugly record-keeping.
197 ((MoveCommand)c).moveAgain(difference.getX(), difference.getY());
198 lastTranslatedN1en = newN1en;
199 } else {
200 Collection<OsmPrimitive> nodelist = new LinkedList<OsmPrimitive>();
201 nodelist.add(n1);
202 nodelist.add(n2);
203 Main.main.undoRedo.add(c = new MoveCommand(nodelist, difference.getX(), difference.getY()));
204 lastTranslatedN1en = newN1en;
205 }
206 }
207 Main.map.mapView.repaint();
208 }
209 }
210
211 /**
212 * Create a new Line that extends off the edge of the viewport in one direction
213 * @param start The start point of the line
214 * @param unitvector A unit vector denoting the direction of the line
215 * @param g the Graphics2D object it will be used on
216 */
217 static private Line2D createSemiInfiniteLine(Point2D start, Point2D unitvector, Graphics2D g) {
218 Rectangle bounds = g.getDeviceConfiguration().getBounds();
219 try {
220 AffineTransform invtrans = g.getTransform().createInverse();
221 Point2D widthpoint = invtrans.deltaTransform(new Point2D.Double(bounds.width,0), null);
222 Point2D heightpoint = invtrans.deltaTransform(new Point2D.Double(0,bounds.height), null);
223
224 // Here we should end up with a gross overestimate of the maximum viewport diagonal in what
225 // Graphics2D calls 'user space'. Essentially a manhattan distance of manhattan distances.
226 // This can be used as a safe length of line to generate which will always go off-viewport.
227 double linelength = Math.abs(widthpoint.getX()) + Math.abs(widthpoint.getY()) + Math.abs(heightpoint.getX()) + Math.abs(heightpoint.getY());
228
229 return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * linelength) , start.getY() + (unitvector.getY() * linelength)));
230 }
231 catch (NoninvertibleTransformException e) {
232 return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * 10) , start.getY() + (unitvector.getY() * 10)));
233 }
234 }
235
236 public void paint(Graphics2D g, MapView mv, Bounds box) {
237 if (mode == Mode.select) {
238 // Nothing to do
239 } else {
240 if (newN1en != null) {
241 Graphics2D g2 = g;
242 g2.setColor(selectedColor);
243 g2.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
244
245 Point p1 = mv.getPoint(initialN1en);
246 Point p2 = mv.getPoint(initialN2en);
247 Point p3 = mv.getPoint(newN1en);
248 Point p4 = mv.getPoint(newN2en);
249
250 if (mode == Mode.extrude) {
251 // Draw rectangle around new area.
252 GeneralPath b = new GeneralPath();
253 b.moveTo(p1.x, p1.y); b.lineTo(p3.x, p3.y);
254 b.lineTo(p4.x, p4.y); b.lineTo(p2.x, p2.y);
255 b.lineTo(p1.x, p1.y);
256 g2.draw(b);
257 g2.setStroke(new BasicStroke(1));
258 } else if (mode == Mode.translate) {
259 // Highlight the new and old segments.
260 Line2D newline = new Line2D.Double(p3, p4);
261 g2.draw(newline);
262 g2.setStroke(new BasicStroke(1));
263 Line2D oldline = new Line2D.Double(p1, p2);
264 g2.draw(oldline);
265
266 EastNorth segmentVector = new EastNorth(initialN2en.getX()-initialN1en.getX(), initialN2en.getY()-initialN1en.getY());
267
268 double fac = 1.0 / Math.hypot(segmentVector.getX(), segmentVector.getY());
269 // swap coords to get normal, mult by factor to get unit vector.
270 EastNorth normalUnitVector = new EastNorth(segmentVector.getY() * fac, segmentVector.getX() * fac);
271
272 // Draw a guideline along the normal.
273 Line2D normline;
274 Point2D centerpoint = new Point2D.Double((p1.getX()+p2.getX())*0.5, (p1.getY()+p2.getY())*0.5);
275 EastNorth drawnorm;
276 // Check to see if our new N1 is in a positive direction with respect to the normalUnitVector.
277 // Even if the x component is zero, we should still be able to discern using +0.0 and -0.0
278 if (newN1en == null || (newN1en.getX() > initialN1en.getX() == normalUnitVector.getX() > -0.0)) {
279 drawnorm = normalUnitVector;
280 } else {
281 // If not, use a sign-flipped version of the normalUnitVector.
282 drawnorm = new EastNorth(-normalUnitVector.getX(), -normalUnitVector.getY());
283 }
284 normline = createSemiInfiniteLine(centerpoint, drawnorm, g2);
285 g2.draw(normline);
286
287 // EastNorth units per pixel
288 double factor = 1.0/g2.getTransform().getScaleX();
289
290 // Draw right angle marker on initial position.
291 double raoffsetx = 8.0*factor*drawnorm.getX();
292 double raoffsety = 8.0*factor*drawnorm.getY();
293 Point2D ra1 = new Point2D.Double(centerpoint.getX()+raoffsetx, centerpoint.getY()+raoffsety);
294 Point2D ra3 = new Point2D.Double(centerpoint.getX()-raoffsety, centerpoint.getY()+raoffsetx);
295 Point2D ra2 = new Point2D.Double(ra1.getX()-raoffsety, ra1.getY()+raoffsetx);
296 GeneralPath ra = new GeneralPath();
297 ra.moveTo((float)ra1.getX(), (float)ra1.getY());
298 ra.lineTo((float)ra2.getX(), (float)ra2.getY());
299 ra.lineTo((float)ra3.getX(), (float)ra3.getY());
300 g2.draw(ra);
301 }
302 }
303 }
304 }
305
306 /**
307 * If the left mouse button is pressed over a segment, switch
308 * to either extrude or translate mode depending on whether ctrl is held.
309 */
310 @Override public void mousePressed(MouseEvent e) {
311 if(!Main.map.mapView.isActiveLayerVisible())
312 return;
313 if (!(Boolean)this.getValue("active")) return;
314 if (e.getButton() != MouseEvent.BUTTON1)
315 return;
316 // boolean ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0;
317 // boolean alt = (e.getModifiers() & ActionEvent.ALT_MASK) != 0;
318 // boolean shift = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0;
319
320 selectedSegment = Main.map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive.isSelectablePredicate);
321
322 if (selectedSegment == null) {
323 // If nothing gets caught, stay in select mode
324 } else {
325 // Otherwise switch to another mode
326
327 // For extrusion, these positions are actually never changed,
328 // but keeping note of this anyway allows us to not continually
329 // look it up and also allows us to unify code with the translate mode
330 initialN1en = selectedSegment.way.getNode(selectedSegment.lowerIndex).getEastNorth();
331 initialN2en = selectedSegment.way.getNode(selectedSegment.lowerIndex + 1).getEastNorth();
332
333 // Signifies that nothing has happened yet
334 newN1en = null;
335 newN2en = null;
336
337 Main.map.mapView.addTemporaryLayer(this);
338
339 updateStatusLine();
340 Main.map.mapView.repaint();
341
342 // Make note of time pressed
343 mouseDownTime = System.currentTimeMillis();
344
345 // Make note of mouse position
346 initialMousePos = e.getPoint();
347
348 // Switch mode.
349 if ( (e.getModifiers() & ActionEvent.CTRL_MASK) != 0 ) {
350 mode = Mode.translate;
351 lastTranslatedN1en = initialN1en;
352 } else {
353 mode = Mode.extrude;
354 getCurrentDataSet().setSelected(selectedSegment.way);
355 }
356 }
357 }
358
359 /**
360 * Do anything that needs to be done, then switch back to select mode
361 */
362 @Override public void mouseReleased(MouseEvent e) {
363
364 if(!Main.map.mapView.isActiveLayerVisible())
365 return;
366
367 if (mode == Mode.select) {
368 // Nothing to be done
369 } else {
370 if (mode == Mode.extrude) {
371 if (e.getPoint().distance(initialMousePos) > 10 && newN1en != null) {
372 // Commit extrusion
373
374 Node n1 = selectedSegment.way.getNode(selectedSegment.lowerIndex);
375 //Node n2 = selectedSegment.way.getNode(selectedSegment.lowerIndex+1);
376 Node n3 = new Node(Main.proj.eastNorth2latlon(newN2en));
377 Node n4 = new Node(Main.proj.eastNorth2latlon(newN1en));
378 Way wnew = new Way(selectedSegment.way);
379 wnew.addNode(selectedSegment.lowerIndex+1, n3);
380 wnew.addNode(selectedSegment.lowerIndex+1, n4);
381 if (wnew.getNodesCount() == 4) {
382 wnew.addNode(n1);
383 }
384 Collection<Command> cmds = new LinkedList<Command>();
385 cmds.add(new AddCommand(n4));
386 cmds.add(new AddCommand(n3));
387 cmds.add(new ChangeCommand(selectedSegment.way, wnew));
388 Command c = new SequenceCommand(tr("Extrude Way"), cmds);
389 Main.main.undoRedo.add(c);
390 }
391 } else if (mode == Mode.translate) {
392 // I don't think there's anything to do
393 }
394
395 // Switch back into select mode
396 restoreCursor();
397 Main.map.mapView.removeTemporaryLayer(this);
398 selectedSegment = null;
399 mode = Mode.select;
400
401 updateStatusLine();
402 Main.map.mapView.repaint();
403 }
404 }
405
406 @Override public String getModeHelpText() {
407 if (mode == Mode.translate)
408 return tr("Move a segment along its normal, then release the mouse button.");
409 else if (mode == Mode.extrude)
410 return tr("Draw a rectangle of the desired size, then release the mouse button.");
411 else
412 return tr("Drag a way segment to make a rectangle. Ctrl-drag to move a segment along its normal.");
413 }
414
415 @Override public boolean layerIsSupported(Layer l) {
416 return l instanceof OsmDataLayer;
417 }
418}
Note: See TracBrowser for help on using the repository browser.