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

Last change on this file since 2692 was 2666, checked in by jttt, 14 years ago

Minor mappaint cleanup, use constants for colors

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