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

Last change on this file since 1054 was 1054, checked in by stoecker, 16 years ago

fixed a lot of the shortcut related translations

  • Property svn:eol-style set to native
File size: 19.2 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.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.SelectionChangedListener;
41import org.openstreetmap.josm.data.coor.EastNorth;
42import org.openstreetmap.josm.data.coor.LatLon;
43import org.openstreetmap.josm.data.osm.DataSet;
44import org.openstreetmap.josm.data.osm.Node;
45import org.openstreetmap.josm.data.osm.OsmPrimitive;
46import org.openstreetmap.josm.data.osm.Way;
47import org.openstreetmap.josm.data.osm.WaySegment;
48import org.openstreetmap.josm.gui.MapFrame;
49import org.openstreetmap.josm.gui.MapView;
50import org.openstreetmap.josm.gui.layer.MapViewPaintable;
51import org.openstreetmap.josm.tools.ImageProvider;
52import org.openstreetmap.josm.tools.Pair;
53import org.openstreetmap.josm.tools.ShortCut;
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 ShortCut.registerShortCut("mapmode:draw", tr("Mode: {0}", tr("Draw")), KeyEvent.VK_A, ShortCut.GROUP_EDIT),
77 mapFrame, getCursor());
78
79 // Add extra shortcut N
80 Main.contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
81 ShortCut.registerShortCut("mapmode:drawfocus", tr("Mode: Draw Focus"), KeyEvent.VK_N, ShortCut.GROUP_EDIT).getKeyStroke(), tr("Draw"));
82
83 //putValue("help", "Action/AddNode/Autnode");
84 selectedColor = Main.pref.getColor(marktr("selected"), Color.YELLOW);
85
86 drawHelperLine = Main.pref.getBoolean("draw.helper-line", true);
87 }
88
89 private static Cursor getCursor() {
90 try {
91 return ImageProvider.getCursor("crosshair", null);
92 } catch (Exception e) {
93 }
94 return Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
95 }
96
97 @Override public void enterMode() {
98 super.enterMode();
99 Main.map.mapView.addMouseListener(this);
100 Main.map.mapView.addMouseMotionListener(this);
101 Main.map.mapView.addTemporaryLayer(this);
102 DataSet.selListeners.add(this);
103 try {
104 Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK);
105 } catch (SecurityException ex) {
106 }
107 // would like to but haven't got mouse position yet:
108 // computeHelperLine(false, false, false);
109 }
110 @Override public void exitMode() {
111 super.exitMode();
112 Main.map.mapView.removeMouseListener(this);
113 Main.map.mapView.removeMouseMotionListener(this);
114 Main.map.mapView.removeTemporaryLayer(this);
115 DataSet.selListeners.remove(this);
116 try {
117 Toolkit.getDefaultToolkit().removeAWTEventListener(this);
118 } catch (SecurityException ex) {
119 }
120 }
121
122 /**
123 * redraw to (possibly) get rid of helper line if selection changes.
124 */
125 public void eventDispatched(AWTEvent event) {
126 if(!Main.map.mapView.isDrawableLayer())
127 return;
128 InputEvent e = (InputEvent) event;
129 ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0;
130 alt = (e.getModifiers() & ActionEvent.ALT_MASK) != 0;
131 shift = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0;
132 computeHelperLine();
133 }
134 /**
135 * redraw to (possibly) get rid of helper line if selection changes.
136 */
137 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
138 if(!Main.map.mapView.isDrawableLayer())
139 return;
140 computeHelperLine();
141 }
142
143 /**
144 * If user clicked with the left button, add a node at the current mouse
145 * position.
146 *
147 * If in nodeway mode, insert the node into the way.
148 */
149 @Override public void mouseClicked(MouseEvent e) {
150
151 if (e.getButton() != MouseEvent.BUTTON1)
152 return;
153 if(!Main.map.mapView.isDrawableLayer())
154 return;
155
156 // we copy ctrl/alt/shift from the event just in case our global
157 // AWTEvent didn't make it through the security manager. Unclear
158 // if that can ever happen but better be safe.
159 ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0;
160 alt = (e.getModifiers() & ActionEvent.ALT_MASK) != 0;
161 shift = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0;
162 mousePos = e.getPoint();
163
164 Collection<OsmPrimitive> selection = Main.ds.getSelected();
165 Collection<Command> cmds = new LinkedList<Command>();
166
167 ArrayList<Way> reuseWays = new ArrayList<Way>(),
168 replacedWays = new ArrayList<Way>();
169 boolean newNode = false;
170 Node n = null;
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
285 Way way = alt ? null : (selectedWay != null) ? selectedWay : getWayForNode(n0);
286 if (way == null) {
287 way = new Way();
288 way.nodes.add(n0);
289 cmds.add(new AddCommand(way));
290 } else {
291 int i;
292 if ((i = replacedWays.indexOf(way)) != -1) {
293 way = reuseWays.get(i);
294 } else {
295 Way wnew = new Way(way);
296 cmds.add(new ChangeCommand(way, wnew));
297 way = wnew;
298 }
299 }
300
301 if (way.nodes.get(way.nodes.size() - 1) == n0) {
302 way.nodes.add(n);
303 } else {
304 way.nodes.add(0, n);
305 }
306
307 extendedWay = true;
308 Main.ds.setSelected(way);
309 }
310
311 String title;
312 if (!extendedWay && !newNode) {
313 return; // We didn't do anything.
314 } else if (!extendedWay) {
315 if (reuseWays.isEmpty()) {
316 title = tr("Add node");
317 } else {
318 title = tr("Add node into way");
319 }
320 for (Way w : reuseWays) w.selected = false;
321 Main.ds.setSelected(n);
322 } else if (!newNode) {
323 title = tr("Connect existing way to node");
324 } else if (reuseWays.isEmpty()) {
325 title = tr("Add a new node to an existing way");
326 } else {
327 title = tr("Add node into way and connect");
328 }
329
330 Command c = new SequenceCommand(title, cmds);
331
332 Main.main.undoRedo.add(c);
333 lastUsedNode = n;
334 computeHelperLine();
335 Main.map.mapView.repaint();
336 }
337
338 @Override public void mouseMoved(MouseEvent e) {
339 if(!Main.map.mapView.isDrawableLayer())
340 return;
341
342 // we copy ctrl/alt/shift from the event just in case our global
343 // AWTEvent didn't make it through the security manager. Unclear
344 // if that can ever happen but better be safe.
345
346 ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0;
347 alt = (e.getModifiers() & ActionEvent.ALT_MASK) != 0;
348 shift = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0;
349 mousePos = e.getPoint();
350
351 computeHelperLine();
352 }
353
354 /**
355 * This method prepares data required for painting the "helper line" from
356 * the last used position to the mouse cursor. It duplicates some code from
357 * mouseClicked() (FIXME).
358 */
359 private void computeHelperLine() {
360 if (mousePos == null) {
361 // Don't draw the line.
362 currentMouseEastNorth = null;
363 currentBaseNode = null;
364 return;
365 }
366
367 double distance = -1;
368 double angle = -1;
369
370 Collection<OsmPrimitive> selection = Main.ds.getSelected();
371
372 Node selectedNode = null;
373 Way selectedWay = null;
374 Node currentMouseNode = null;
375 mouseOnExistingNode = false;
376
377 Main.map.statusLine.setAngle(-1);
378 Main.map.statusLine.setHeading(-1);
379 Main.map.statusLine.setDist(-1);
380
381 if (!ctrl && mousePos != null) {
382 currentMouseNode = Main.map.mapView.getNearestNode(mousePos);
383 }
384
385 if (currentMouseNode != null) {
386 // user clicked on node
387 if (selection.isEmpty()) return;
388 currentMouseEastNorth = currentMouseNode.eastNorth;
389 mouseOnExistingNode = true;
390 } else {
391 // no node found in clicked area
392 currentMouseEastNorth = Main.map.mapView.getEastNorth(mousePos.x, mousePos.y);
393 }
394
395 for (OsmPrimitive p : selection) {
396 if (p instanceof Node) {
397 if (selectedNode != null) return;
398 selectedNode = (Node) p;
399 } else if (p instanceof Way) {
400 if (selectedWay != null) return;
401 selectedWay = (Way) p;
402 }
403 }
404
405 // the node from which we make a connection
406 currentBaseNode = null;
407 Node previousNode = null;
408
409 if (selectedNode == null) {
410 if (selectedWay == null) return;
411 if (lastUsedNode == selectedWay.nodes.get(0) || lastUsedNode == selectedWay.nodes.get(selectedWay.nodes.size()-1)) {
412 currentBaseNode = lastUsedNode;
413 if (lastUsedNode == selectedWay.nodes.get(selectedWay.nodes.size()-1) && selectedWay.nodes.size() > 1) {
414 previousNode = selectedWay.nodes.get(selectedWay.nodes.size()-2);
415 }
416 }
417 } else if (selectedWay == null) {
418 currentBaseNode = selectedNode;
419 } else {
420 if (selectedNode == selectedWay.nodes.get(0) || selectedNode == selectedWay.nodes.get(selectedWay.nodes.size()-1)) {
421 currentBaseNode = selectedNode;
422 }
423 }
424
425 if (currentBaseNode == null || currentBaseNode == currentMouseNode) {
426 return; // Don't create zero length way segments.
427 }
428
429 // find out the distance, in metres, between the base point and the mouse cursor
430 LatLon mouseLatLon = Main.proj.eastNorth2latlon(currentMouseEastNorth);
431 distance = currentBaseNode.coor.greatCircleDistance(mouseLatLon);
432 double hdg = Math.toDegrees(currentBaseNode.coor.heading(mouseLatLon));
433 if (previousNode != null) {
434 angle = hdg - Math.toDegrees(previousNode.coor.heading(currentBaseNode.coor));
435 if (angle < 0) angle += 360;
436 }
437 Main.map.statusLine.setAngle(angle);
438 Main.map.statusLine.setHeading(hdg);
439 Main.map.statusLine.setDist(distance);
440 updateStatusLine();
441
442 if (!drawHelperLine) return;
443
444 Main.map.mapView.repaint();
445 }
446
447 /**
448 * Repaint on mouse exit so that the helper line goes away.
449 */
450 @Override public void mouseExited(MouseEvent e) {
451 if(!Main.map.mapView.isDrawableLayer())
452 return;
453 mousePos = e.getPoint();
454 Main.map.mapView.repaint();
455 }
456
457 /**
458 * @return If the node is the end of exactly one way, return this.
459 * <code>null</code> otherwise.
460 */
461 public static Way getWayForNode(Node n) {
462 Way way = null;
463 for (Way w : Main.ds.ways) {
464 if (w.deleted || w.incomplete || w.nodes.size() < 1) continue;
465 Node firstNode = w.nodes.get(0);
466 Node lastNode = w.nodes.get(w.nodes.size() - 1);
467 if ((firstNode == n || lastNode == n) && (firstNode != lastNode)) {
468 if (way != null)
469 return null;
470 way = w;
471 }
472 }
473 return way;
474 }
475
476 private static void pruneSuccsAndReverse(List<Integer> is) {
477 //if (is.size() < 2) return;
478
479 HashSet<Integer> is2 = new HashSet<Integer>();
480 for (int i : is) {
481 if (!is2.contains(i - 1) && !is2.contains(i + 1)) {
482 is2.add(i);
483 }
484 }
485 is.clear();
486 is.addAll(is2);
487 Collections.sort(is);
488 Collections.reverse(is);
489 }
490
491 /**
492 * Adjusts the position of a node to lie on a segment (or a segment
493 * intersection).
494 *
495 * If one or more than two segments are passed, the node is adjusted
496 * to lie on the first segment that is passed.
497 *
498 * If two segments are passed, the node is adjusted to be at their
499 * intersection.
500 *
501 * No action is taken if no segments are passed.
502 *
503 * @param segs the segments to use as a reference when adjusting
504 * @param n the node to adjust
505 */
506 private static void adjustNode(Collection<Pair<Node,Node>> segs, Node n) {
507
508 switch (segs.size()) {
509 case 0:
510 return;
511 case 2:
512 // This computes the intersection between
513 // the two segments and adjusts the node position.
514 Iterator<Pair<Node,Node>> i = segs.iterator();
515 Pair<Node,Node> seg = i.next();
516 EastNorth A = seg.a.eastNorth;
517 EastNorth B = seg.b.eastNorth;
518 seg = i.next();
519 EastNorth C = seg.a.eastNorth;
520 EastNorth D = seg.b.eastNorth;
521
522 double u=det(B.east() - A.east(), B.north() - A.north(), C.east() - D.east(), C.north() - D.north());
523
524 // Check for parallel segments and do nothing if they are
525 // In practice this will probably only happen when a way has been duplicated
526
527 if (u == 0) return;
528
529 // q is a number between 0 and 1
530 // It is the point in the segment where the intersection occurs
531 // if the segment is scaled to lenght 1
532
533 double q = det(B.north() - C.north(), B.east() - C.east(), D.north() - C.north(), D.east() - C.east()) / u;
534 EastNorth intersection = new EastNorth(
535 B.east() + q * (A.east() - B.east()),
536 B.north() + q * (A.north() - B.north()));
537
538 int snapToIntersectionThreshold=0;
539 try { snapToIntersectionThreshold = Integer.parseInt(Main.pref.get("edit.snap-intersection-threshold","10")); } catch (NumberFormatException x) {}
540
541 // only adjust to intersection if within snapToIntersectionThreshold pixel of mouse click; otherwise
542 // fall through to default action.
543 // (for semi-parallel lines, intersection might be miles away!)
544 if (Main.map.mapView.getPoint(n.eastNorth).distance(Main.map.mapView.getPoint(intersection)) < snapToIntersectionThreshold) {
545 n.eastNorth = intersection;
546 return;
547 }
548
549 default:
550 EastNorth P = n.eastNorth;
551 seg = segs.iterator().next();
552 A = seg.a.eastNorth;
553 B = seg.b.eastNorth;
554 double a = P.distanceSq(B);
555 double b = P.distanceSq(A);
556 double c = A.distanceSq(B);
557 q = (a - b + c) / (2*c);
558 n.eastNorth = new EastNorth(
559 B.east() + q * (A.east() - B.east()),
560 B.north() + q * (A.north() - B.north()));
561 }
562 }
563
564 // helper for adjustNode
565 static double det(double a, double b, double c, double d)
566 {
567 return a * d - b * c;
568 }
569
570 public void paint(Graphics g, MapView mv) {
571
572 // don't draw line if disabled in prefs
573 if (!drawHelperLine) return;
574
575 // sanity checks
576 if (Main.map.mapView == null) return;
577 if (mousePos == null) return;
578
579 // if shift key is held ("no auto-connect"), don't draw a line
580 if (shift) return;
581
582 // don't draw line if we don't know where from or where to
583 if (currentBaseNode == null) return;
584 if (currentMouseEastNorth == null) return;
585
586 // don't draw line if mouse is outside window
587 if (!Main.map.mapView.getBounds().contains(mousePos)) return;
588
589 Graphics2D g2 = (Graphics2D) g;
590 g2.setColor(selectedColor);
591 g2.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
592 GeneralPath b = new GeneralPath();
593 Point p1=mv.getPoint(currentBaseNode.eastNorth);
594 Point p2=mv.getPoint(currentMouseEastNorth);
595
596 double t = Math.atan2(p2.y-p1.y, p2.x-p1.x) + Math.PI;
597
598 b.moveTo(p1.x,p1.y); b.lineTo(p2.x, p2.y);
599
600 // if alt key is held ("start new way"), draw a little perpendicular line
601 if (alt) {
602 b.moveTo((int)(p1.x + 8*Math.cos(t+PHI)), (int)(p1.y + 8*Math.sin(t+PHI)));
603 b.lineTo((int)(p1.x + 8*Math.cos(t-PHI)), (int)(p1.y + 8*Math.sin(t-PHI)));
604 }
605
606 g2.draw(b);
607 g2.setStroke(new BasicStroke(1));
608
609 }
610
611 @Override public String getModeHelpText() {
612 String rv;
613
614 if (currentBaseNode != null && !shift) {
615 if (mouseOnExistingNode) {
616 if (alt && /* FIXME: way exists */true)
617 rv = tr("Click to create a new way to the existing node.");
618 else
619 rv =tr("Click to make a connection to the existing node.");
620 } else {
621 if (alt && /* FIXME: way exists */true)
622 rv = tr("Click to insert a node and create a new way.");
623 else
624 rv = tr("Click to insert a new node and make a connection.");
625 }
626 }
627 else {
628 rv = tr("Click to insert a new node.");
629 }
630
631 //rv.append(tr("Click to add a new node. Ctrl: no node re-use/auto-insert. Shift: no auto-connect. Alt: new way"));
632 //rv.append(tr("Click to add a new node. Ctrl: no node re-use/auto-insert. Shift: no auto-connect. Alt: new way"));
633 return rv.toString();
634 }
635}
Note: See TracBrowser for help on using the repository browser.