source: josm/trunk/src/org/openstreetmap/josm/actions/mapmode/DrawAction.java@ 644

Last change on this file since 644 was 644, checked in by david, 16 years ago

Allow SHIFT+CLICK in Draw Nodes mode to select node whether or not anything is selected (That gesture previously did nothing if there was something already selected)

  • Property svn:eol-style set to native
File size: 18.6 KB
Line 
1// License: GPL. See LICENSE file for details.
2package org.openstreetmap.josm.actions.mapmode;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.AWTEvent;
7import java.awt.BasicStroke;
8import java.awt.Color;
9import java.awt.Cursor;
10import java.awt.Graphics;
11import java.awt.Graphics2D;
12import java.awt.Point;
13import java.awt.Toolkit;
14import java.awt.event.AWTEventListener;
15import java.awt.event.ActionEvent;
16import java.awt.event.InputEvent;
17import java.awt.event.KeyEvent;
18import java.awt.event.MouseEvent;
19import java.awt.geom.GeneralPath;
20import java.util.ArrayList;
21import java.util.Collection;
22import java.util.Collections;
23import java.util.HashMap;
24import java.util.HashSet;
25import java.util.Iterator;
26import java.util.LinkedList;
27import java.util.List;
28import java.util.Map;
29import java.util.Set;
30
31import javax.swing.JComponent;
32import javax.swing.JOptionPane;
33import javax.swing.KeyStroke;
34
35import org.openstreetmap.josm.Main;
36import org.openstreetmap.josm.command.AddCommand;
37import org.openstreetmap.josm.command.ChangeCommand;
38import org.openstreetmap.josm.command.Command;
39import org.openstreetmap.josm.command.SequenceCommand;
40import org.openstreetmap.josm.data.Preferences;
41import org.openstreetmap.josm.data.SelectionChangedListener;
42import org.openstreetmap.josm.data.coor.EastNorth;
43import org.openstreetmap.josm.data.coor.LatLon;
44import org.openstreetmap.josm.data.osm.DataSet;
45import org.openstreetmap.josm.data.osm.Node;
46import org.openstreetmap.josm.data.osm.OsmPrimitive;
47import org.openstreetmap.josm.data.osm.Way;
48import org.openstreetmap.josm.data.osm.WaySegment;
49import org.openstreetmap.josm.gui.MapFrame;
50import org.openstreetmap.josm.gui.MapView;
51import org.openstreetmap.josm.gui.layer.MapViewPaintable;
52import org.openstreetmap.josm.tools.ImageProvider;
53import org.openstreetmap.josm.tools.Pair;
54
55/**
56 *
57 */
58public class DrawAction extends MapMode implements MapViewPaintable, SelectionChangedListener, AWTEventListener {
59
60 private static Node lastUsedNode = null;
61 private double PHI=Math.toRadians(90);
62
63 private boolean ctrl;
64 private boolean alt;
65 private boolean shift;
66 private boolean mouseOnExistingNode;
67 private boolean drawHelperLine;
68 private Point mousePos;
69 private Color selectedColor;
70
71 private Node currentBaseNode;
72 private EastNorth currentMouseEastNorth;
73
74 public DrawAction(MapFrame mapFrame) {
75 super(tr("Draw"), "node/autonode", tr("Draw nodes"),
76 KeyEvent.VK_A, mapFrame, getCursor());
77
78 // Add extra shortcut N
79 Main.contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
80 KeyStroke.getKeyStroke(KeyEvent.VK_N, 0), tr("Draw"));
81
82 //putValue("help", "Action/AddNode/Autnode");
83 selectedColor = Preferences.getPreferencesColor("selected", Color.YELLOW);
84
85 drawHelperLine = Main.pref.getBoolean("draw.helper-line", true);
86 }
87
88 private static Cursor getCursor() {
89 try {
90 return ImageProvider.getCursor("crosshair", null);
91 } catch (Exception e) {
92 }
93 return Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
94 }
95
96 @Override public void enterMode() {
97 super.enterMode();
98 Main.map.mapView.addMouseListener(this);
99 Main.map.mapView.addMouseMotionListener(this);
100 Main.map.mapView.addTemporaryLayer(this);
101 DataSet.selListeners.add(this);
102 try {
103 Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK);
104 } catch (SecurityException ex) {
105 }
106 // would like to but haven't got mouse position yet:
107 // computeHelperLine(false, false, false);
108 }
109 @Override public void exitMode() {
110 super.exitMode();
111 Main.map.mapView.removeMouseListener(this);
112 Main.map.mapView.removeMouseMotionListener(this);
113 Main.map.mapView.removeTemporaryLayer(this);
114 DataSet.selListeners.remove(this);
115 try {
116 Toolkit.getDefaultToolkit().removeAWTEventListener(this);
117 } catch (SecurityException ex) {
118 }
119 }
120
121 /**
122 * redraw to (possibly) get rid of helper line if selection changes.
123 */
124 public void eventDispatched(AWTEvent event) {
125 InputEvent e = (InputEvent) event;
126 ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0;
127 alt = (e.getModifiers() & ActionEvent.ALT_MASK) != 0;
128 shift = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0;
129 computeHelperLine();
130 }
131 /**
132 * redraw to (possibly) get rid of helper line if selection changes.
133 */
134 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
135 computeHelperLine();
136 }
137
138 /**
139 * If user clicked with the left button, add a node at the current mouse
140 * position.
141 *
142 * If in nodeway mode, insert the node into the way.
143 */
144 @Override public void mouseClicked(MouseEvent e) {
145
146 if (e.getButton() != MouseEvent.BUTTON1)
147 return;
148
149 // we copy ctrl/alt/shift from the event just in case our global
150 // AWTEvent didn't make it through the security manager. Unclear
151 // if that can ever happen but better be safe.
152 ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0;
153 alt = (e.getModifiers() & ActionEvent.ALT_MASK) != 0;
154 shift = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0;
155 mousePos = e.getPoint();
156
157 Collection<OsmPrimitive> selection = Main.ds.getSelected();
158 Collection<Command> cmds = new LinkedList<Command>();
159
160 ArrayList<Way> reuseWays = new ArrayList<Way>(),
161 replacedWays = new ArrayList<Way>();
162 boolean newNode = false;
163 Node n = null;
164
165 if (!ctrl) {
166 n = Main.map.mapView.getNearestNode(mousePos);
167 }
168
169 if (n != null) {
170 // user clicked on node
171 if (shift || selection.isEmpty()) {
172 // select the clicked node and do nothing else
173 // (this is just a convenience option so that people don't
174 // have to switch modes)
175 Main.ds.setSelected(n);
176 return;
177 }
178
179 } else {
180 // no node found in clicked area
181 n = new Node(Main.map.mapView.getLatLon(e.getX(), e.getY()));
182 if (n.coor.isOutSideWorld()) {
183 JOptionPane.showMessageDialog(Main.parent,
184 tr("Cannot add a node outside of the world."));
185 return;
186 }
187 newNode = true;
188
189 cmds.add(new AddCommand(n));
190
191 if (!ctrl) {
192 // Insert the node into all the nearby way segments
193 List<WaySegment> wss = Main.map.mapView.getNearestWaySegments(e.getPoint());
194 Map<Way, List<Integer>> insertPoints = new HashMap<Way, List<Integer>>();
195 for (WaySegment ws : wss) {
196 List<Integer> is;
197 if (insertPoints.containsKey(ws.way)) {
198 is = insertPoints.get(ws.way);
199 } else {
200 is = new ArrayList<Integer>();
201 insertPoints.put(ws.way, is);
202 }
203
204 is.add(ws.lowerIndex);
205 }
206
207 Set<Pair<Node,Node>> segSet = new HashSet<Pair<Node,Node>>();
208
209 for (Map.Entry<Way, List<Integer>> insertPoint : insertPoints.entrySet()) {
210 Way w = insertPoint.getKey();
211 List<Integer> is = insertPoint.getValue();
212
213 Way wnew = new Way(w);
214
215 pruneSuccsAndReverse(is);
216 for (int i : is) segSet.add(
217 Pair.sort(new Pair<Node,Node>(w.nodes.get(i), w.nodes.get(i+1))));
218 for (int i : is) wnew.nodes.add(i + 1, n);
219
220 cmds.add(new ChangeCommand(insertPoint.getKey(), wnew));
221 replacedWays.add(insertPoint.getKey());
222 reuseWays.add(wnew);
223 }
224
225 adjustNode(segSet, n);
226 }
227 }
228
229 // This part decides whether or not a "segment" (i.e. a connection) is made to an
230 // existing node.
231
232 // For a connection to be made, the user must either have a node selected (connection
233 // is made to that node), or he must have a way selected *and* one of the endpoints
234 // of that way must be the last used node (connection is made to last used node), or
235 // he must have a way and a node selected (connection is made to the selected node).
236
237 boolean extendedWay = false;
238
239 if (!shift && selection.size() > 0 && selection.size() < 3) {
240
241 Node selectedNode = null;
242 Way selectedWay = null;
243
244 for (OsmPrimitive p : selection) {
245 if (p instanceof Node) {
246 if (selectedNode != null) return;
247 selectedNode = (Node) p;
248 } else if (p instanceof Way) {
249 if (selectedWay != null) return;
250 selectedWay = (Way) p;
251 }
252 }
253
254 // the node from which we make a connection
255 Node n0 = null;
256
257 if (selectedNode == null) {
258 if (selectedWay == null) return;
259 if (lastUsedNode == selectedWay.nodes.get(0) || lastUsedNode == selectedWay.nodes.get(selectedWay.nodes.size()-1)) {
260 n0 = lastUsedNode;
261 }
262 } else if (selectedWay == null) {
263 n0 = selectedNode;
264 } else {
265 if (selectedNode == selectedWay.nodes.get(0) || selectedNode == selectedWay.nodes.get(selectedWay.nodes.size()-1)) {
266 n0 = selectedNode;
267 }
268 }
269
270 if (n0 == null || n0 == n) {
271 return; // Don't create zero length way segments.
272 }
273
274 // Ok we know now that we'll insert a line segment, but will it connect to an
275 // existing way or make a new way of its own? The "alt" modifier means that the
276 // user wants a new way.
277
278 Way way = alt ? null : (selectedWay != null) ? selectedWay : getWayForNode(n0);
279 if (way == null) {
280 way = new Way();
281 way.nodes.add(n0);
282 cmds.add(new AddCommand(way));
283 } else {
284 int i;
285 if ((i = replacedWays.indexOf(way)) != -1) {
286 way = reuseWays.get(i);
287 } else {
288 Way wnew = new Way(way);
289 cmds.add(new ChangeCommand(way, wnew));
290 way = wnew;
291 }
292 }
293
294 if (way.nodes.get(way.nodes.size() - 1) == n0) {
295 way.nodes.add(n);
296 } else {
297 way.nodes.add(0, n);
298 }
299
300 extendedWay = true;
301 Main.ds.setSelected(way);
302 }
303
304 String title;
305 if (!extendedWay && !newNode) {
306 return; // We didn't do anything.
307 } else if (!extendedWay) {
308 if (reuseWays.isEmpty()) {
309 title = tr("Add node");
310 } else {
311 title = tr("Add node into way");
312 }
313 for (Way w : reuseWays) w.selected = false;
314 Main.ds.setSelected(n);
315 } else if (!newNode) {
316 title = tr("Connect existing way to node");
317 } else if (reuseWays.isEmpty()) {
318 title = tr("Add a new node to an existing way");
319 } else {
320 title = tr("Add node into way and connect");
321 }
322
323 Command c = new SequenceCommand(title, cmds);
324
325 Main.main.undoRedo.add(c);
326 lastUsedNode = n;
327 computeHelperLine();
328 Main.map.mapView.repaint();
329 }
330
331 @Override public void mouseMoved(MouseEvent e) {
332
333 // we copy ctrl/alt/shift from the event just in case our global
334 // AWTEvent didn't make it through the security manager. Unclear
335 // if that can ever happen but better be safe.
336
337 ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0;
338 alt = (e.getModifiers() & ActionEvent.ALT_MASK) != 0;
339 shift = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0;
340 mousePos = e.getPoint();
341
342 computeHelperLine();
343 }
344
345 /**
346 * This method prepares data required for painting the "helper line" from
347 * the last used position to the mouse cursor. It duplicates some code from
348 * mouseClicked() (FIXME).
349 */
350 private void computeHelperLine() {
351 if (mousePos == null) {
352 // Don't draw the line.
353 currentMouseEastNorth = null;
354 currentBaseNode = null;
355 return;
356 }
357
358 double distance = -1;
359 double angle = -1;
360
361 Collection<OsmPrimitive> selection = Main.ds.getSelected();
362
363 Node selectedNode = null;
364 Way selectedWay = null;
365 Node currentMouseNode = null;
366 mouseOnExistingNode = false;
367
368 Main.map.statusLine.setAngle(-1);
369 Main.map.statusLine.setHeading(-1);
370 Main.map.statusLine.setDist(-1);
371
372 if (!ctrl && mousePos != null) {
373 currentMouseNode = Main.map.mapView.getNearestNode(mousePos);
374 }
375
376 if (currentMouseNode != null) {
377 // user clicked on node
378 if (selection.isEmpty()) return;
379 currentMouseEastNorth = currentMouseNode.eastNorth;
380 mouseOnExistingNode = true;
381 } else {
382 // no node found in clicked area
383 currentMouseEastNorth = Main.map.mapView.getEastNorth(mousePos.x, mousePos.y);
384 }
385
386 for (OsmPrimitive p : selection) {
387 if (p instanceof Node) {
388 if (selectedNode != null) return;
389 selectedNode = (Node) p;
390 } else if (p instanceof Way) {
391 if (selectedWay != null) return;
392 selectedWay = (Way) p;
393 }
394 }
395
396 // the node from which we make a connection
397 currentBaseNode = null;
398 Node previousNode = null;
399
400 if (selectedNode == null) {
401 if (selectedWay == null) return;
402 if (lastUsedNode == selectedWay.nodes.get(0) || lastUsedNode == selectedWay.nodes.get(selectedWay.nodes.size()-1)) {
403 currentBaseNode = lastUsedNode;
404 if (lastUsedNode == selectedWay.nodes.get(selectedWay.nodes.size()-1) && selectedWay.nodes.size() > 1) {
405 previousNode = selectedWay.nodes.get(selectedWay.nodes.size()-2);
406 }
407 }
408 } else if (selectedWay == null) {
409 currentBaseNode = selectedNode;
410 } else {
411 if (selectedNode == selectedWay.nodes.get(0) || selectedNode == selectedWay.nodes.get(selectedWay.nodes.size()-1)) {
412 currentBaseNode = selectedNode;
413 }
414 }
415
416 if (currentBaseNode == null || currentBaseNode == currentMouseNode) {
417 return; // Don't create zero length way segments.
418 }
419
420 // find out the distance, in metres, between the base point and the mouse cursor
421 LatLon mouseLatLon = Main.proj.eastNorth2latlon(currentMouseEastNorth);
422 distance = currentBaseNode.coor.greatCircleDistance(mouseLatLon);
423 double hdg = Math.toDegrees(currentBaseNode.coor.heading(mouseLatLon));
424 if (previousNode != null) {
425 angle = hdg - Math.toDegrees(previousNode.coor.heading(currentBaseNode.coor));
426 if (angle < 0) angle += 360;
427 }
428 Main.map.statusLine.setAngle(angle);
429 Main.map.statusLine.setHeading(hdg);
430 Main.map.statusLine.setDist(distance);
431 updateStatusLine();
432
433 if (!drawHelperLine) return;
434
435 Main.map.mapView.repaint();
436 }
437
438 /**
439 * Repaint on mouse exit so that the helper line goes away.
440 */
441 @Override public void mouseExited(MouseEvent e) {
442 mousePos = e.getPoint();
443 Main.map.mapView.repaint();
444 }
445
446 /**
447 * @return If the node is the end of exactly one way, return this.
448 * <code>null</code> otherwise.
449 */
450 public static Way getWayForNode(Node n) {
451 Way way = null;
452 for (Way w : Main.ds.ways) {
453 if (w.deleted || w.incomplete || w.nodes.size() < 1) continue;
454 Node firstNode = w.nodes.get(0);
455 Node lastNode = w.nodes.get(w.nodes.size() - 1);
456 if ((firstNode == n || lastNode == n) && (firstNode != lastNode)) {
457 if (way != null)
458 return null;
459 way = w;
460 }
461 }
462 return way;
463 }
464
465 private static void pruneSuccsAndReverse(List<Integer> is) {
466 //if (is.size() < 2) return;
467
468 HashSet<Integer> is2 = new HashSet<Integer>();
469 for (int i : is) {
470 if (!is2.contains(i - 1) && !is2.contains(i + 1)) {
471 is2.add(i);
472 }
473 }
474 is.clear();
475 is.addAll(is2);
476 Collections.sort(is);
477 Collections.reverse(is);
478 }
479
480 /*
481 * Adjusts the position of a node to lie on a segment (or a segment
482 * intersection).
483 *
484 * If one or more than two segments are passed, the node is adjusted
485 * to lie on the first segment that is passed.
486 *
487 * If two segments are passed, the node is adjusted to be at their
488 * intersection.
489 *
490 * No action is taken if no segments are passed.
491 *
492 * @param segs the segments to use as a reference when adjusting
493 * @param n the node to adjust
494 */
495 private static void adjustNode(Collection<Pair<Node,Node>> segs, Node n) {
496
497 switch (segs.size()) {
498 case 0:
499 return;
500 case 2:
501 // algorithm used here is a bit clumsy, anyone's welcome to replace
502 // it by something else. All it does it compute the intersection between
503 // the two segments and adjust the node position. The code doesnt
504 Iterator<Pair<Node,Node>> i = segs.iterator();
505 Pair<Node,Node> seg = i.next();
506 EastNorth A = seg.a.eastNorth;
507 EastNorth B = seg.b.eastNorth;
508 seg = i.next();
509 EastNorth C = seg.a.eastNorth;
510 EastNorth D = seg.b.eastNorth;
511
512 EastNorth intersection = new EastNorth(
513 det(det(A.east(), A.north(), B.east(), B.north()), A.east() - B.east(),
514 det(C.east(), C.north(), D.east(), D.north()), C.east() - D.east())/
515 det(A.east() - B.east(), A.north() - B.north(), C.east() - D.east(), C.north() - D.north()),
516 det(det(A.east(), A.north(), B.east(), B.north()), A.north() - B.north(),
517 det(C.east(), C.north(), D.east(), D.north()), C.north() - D.north())/
518 det(A.east() - B.east(), A.north() - B.north(), C.east() - D.east(), C.north() - D.north())
519 );
520
521 // only adjust to intersection if within 10 pixel of mouse click; otherwise
522 // fall through to default action.
523 // (for semi-parallel lines, intersection might be miles away!)
524 if (Main.map.mapView.getPoint(n.eastNorth).distance(Main.map.mapView.getPoint(intersection)) < 10) {
525 n.eastNorth = intersection;
526 return;
527 }
528
529 default:
530 EastNorth P = n.eastNorth;
531 seg = segs.iterator().next();
532 A = seg.a.eastNorth;
533 B = seg.b.eastNorth;
534 double a = P.distanceSq(B);
535 double b = P.distanceSq(A);
536 double c = A.distanceSq(B);
537 double q = (a - b + c) / (2*c);
538 n.eastNorth = new EastNorth(
539 B.east() + q * (A.east() - B.east()),
540 B.north() + q * (A.north() - B.north()));
541 }
542 }
543
544 // helper for adjustNode
545 static double det(double a, double b, double c, double d)
546 {
547 return a * d - b * c;
548 }
549
550 public void paint(Graphics g, MapView mv) {
551
552 // don't draw line if disabled in prefs
553 if (!drawHelperLine) return;
554
555 // sanity checks
556 if (Main.map.mapView == null) return;
557 if (mousePos == null) return;
558
559 // if shift key is held ("no auto-connect"), don't draw a line
560 if (shift) return;
561
562 // don't draw line if we don't know where from or where to
563 if (currentBaseNode == null) return;
564 if (currentMouseEastNorth == null) return;
565
566 // don't draw line if mouse is outside window
567 if (!Main.map.mapView.getBounds().contains(mousePos)) return;
568
569 Graphics2D g2 = (Graphics2D) g;
570 g2.setColor(selectedColor);
571 g2.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
572 GeneralPath b = new GeneralPath();
573 Point p1=mv.getPoint(currentBaseNode.eastNorth);
574 Point p2=mv.getPoint(currentMouseEastNorth);
575
576 double t = Math.atan2(p2.y-p1.y, p2.x-p1.x) + Math.PI;
577
578 b.moveTo(p1.x,p1.y); b.lineTo(p2.x, p2.y);
579
580 // if alt key is held ("start new way"), draw a little perpendicular line
581 if (alt) {
582 b.moveTo((int)(p1.x + 8*Math.cos(t+PHI)), (int)(p1.y + 8*Math.sin(t+PHI)));
583 b.lineTo((int)(p1.x + 8*Math.cos(t-PHI)), (int)(p1.y + 8*Math.sin(t-PHI)));
584 }
585
586 g2.draw(b);
587 g2.setStroke(new BasicStroke(1));
588
589 }
590
591 @Override public String getModeHelpText() {
592 String rv;
593
594 if (currentBaseNode != null && !shift) {
595 if (mouseOnExistingNode) {
596 if (alt && /* FIXME: way exists */true)
597 rv = tr("Click to create a new way to the existing node.");
598 else
599 rv =tr("Click to make a connection to the existing node.");
600 } else {
601 if (alt && /* FIXME: way exists */true)
602 rv = tr("Click to insert a node and create a new way.");
603 else
604 rv = tr("Click to insert a new node and make a connection.");
605 }
606 }
607 else {
608 rv = tr("Click to insert a new node.");
609 }
610
611 //rv.append(tr("Click to add a new node. Ctrl: no node re-use/auto-insert. Shift: no auto-connect. Alt: new way"));
612 //rv.append(tr("Click to add a new node. Ctrl: no node re-use/auto-insert. Shift: no auto-connect. Alt: new way"));
613 return rv.toString();
614 }
615}
Note: See TracBrowser for help on using the repository browser.