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

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

fix #2025. Patch by xeen. Hopefully this has no side effects

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