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

Last change on this file since 1287 was 1287, checked in by stoecker, 15 years ago

close #381 and #696. Patch by xeen

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