source: josm/trunk/src/org/openstreetmap/josm/actions/mapmode/ImproveWayAccuracyAction.java@ 10853

Last change on this file since 10853 was 10853, checked in by Don-vip, 8 years ago

see #13309 - fix most of deprecation warnings

  • Property svn:eol-style set to native
File size: 24.8 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions.mapmode;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.Color;
9import java.awt.Cursor;
10import java.awt.Graphics2D;
11import java.awt.Point;
12import java.awt.Stroke;
13import java.awt.event.KeyEvent;
14import java.awt.event.MouseEvent;
15import java.util.ArrayList;
16import java.util.Collection;
17import java.util.LinkedList;
18import java.util.List;
19
20import javax.swing.JOptionPane;
21
22import org.openstreetmap.josm.Main;
23import org.openstreetmap.josm.command.AddCommand;
24import org.openstreetmap.josm.command.ChangeCommand;
25import org.openstreetmap.josm.command.Command;
26import org.openstreetmap.josm.command.DeleteCommand;
27import org.openstreetmap.josm.command.MoveCommand;
28import org.openstreetmap.josm.command.SequenceCommand;
29import org.openstreetmap.josm.data.Bounds;
30import org.openstreetmap.josm.data.SelectionChangedListener;
31import org.openstreetmap.josm.data.coor.EastNorth;
32import org.openstreetmap.josm.data.osm.DataSet;
33import org.openstreetmap.josm.data.osm.Node;
34import org.openstreetmap.josm.data.osm.OsmPrimitive;
35import org.openstreetmap.josm.data.osm.Way;
36import org.openstreetmap.josm.data.osm.WaySegment;
37import org.openstreetmap.josm.data.osm.visitor.paint.MapPath2D;
38import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
39import org.openstreetmap.josm.data.preferences.ColorProperty;
40import org.openstreetmap.josm.gui.MapFrame;
41import org.openstreetmap.josm.gui.MapView;
42import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
43import org.openstreetmap.josm.gui.layer.AbstractMapViewPaintable;
44import org.openstreetmap.josm.gui.layer.Layer;
45import org.openstreetmap.josm.gui.layer.OsmDataLayer;
46import org.openstreetmap.josm.gui.util.GuiHelper;
47import org.openstreetmap.josm.gui.util.ModifierListener;
48import org.openstreetmap.josm.tools.ImageProvider;
49import org.openstreetmap.josm.tools.Pair;
50import org.openstreetmap.josm.tools.Shortcut;
51
52/**
53 * @author Alexander Kachkaev <alexander@kachkaev.ru>, 2011
54 */
55public class ImproveWayAccuracyAction extends MapMode implements
56 SelectionChangedListener, ModifierListener {
57
58 enum State {
59 selecting, improving
60 }
61
62 private State state;
63
64 private MapView mv;
65
66 private static final long serialVersionUID = 42L;
67
68 private transient Way targetWay;
69 private transient Node candidateNode;
70 private transient WaySegment candidateSegment;
71
72 private Point mousePos;
73 private boolean dragging;
74
75 private final Cursor cursorSelect;
76 private final Cursor cursorSelectHover;
77 private final Cursor cursorImprove;
78 private final Cursor cursorImproveAdd;
79 private final Cursor cursorImproveDelete;
80 private final Cursor cursorImproveAddLock;
81 private final Cursor cursorImproveLock;
82
83 private Color guideColor;
84 private transient Stroke selectTargetWayStroke;
85 private transient Stroke moveNodeStroke;
86 private transient Stroke moveNodeIntersectingStroke;
87 private transient Stroke addNodeStroke;
88 private transient Stroke deleteNodeStroke;
89 private int dotSize;
90
91 private boolean selectionChangedBlocked;
92
93 protected String oldModeHelpText;
94
95 private final transient AbstractMapViewPaintable temporaryLayer = new AbstractMapViewPaintable() {
96 @Override
97 public void paint(Graphics2D g, MapView mv, Bounds bbox) {
98 ImproveWayAccuracyAction.this.paint(g, mv, bbox);
99 }
100 };
101
102 /**
103 * Constructs a new {@code ImproveWayAccuracyAction}.
104 * @param mapFrame Map frame
105 */
106 public ImproveWayAccuracyAction(MapFrame mapFrame) {
107 super(tr("Improve Way Accuracy"), "improvewayaccuracy",
108 tr("Improve Way Accuracy mode"),
109 Shortcut.registerShortcut("mapmode:ImproveWayAccuracy",
110 tr("Mode: {0}", tr("Improve Way Accuracy")),
111 KeyEvent.VK_W, Shortcut.DIRECT), mapFrame, Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
112
113 cursorSelect = ImageProvider.getCursor("normal", "mode");
114 cursorSelectHover = ImageProvider.getCursor("hand", "mode");
115 cursorImprove = ImageProvider.getCursor("crosshair", null);
116 cursorImproveAdd = ImageProvider.getCursor("crosshair", "addnode");
117 cursorImproveDelete = ImageProvider.getCursor("crosshair", "delete_node");
118 cursorImproveAddLock = ImageProvider.getCursor("crosshair",
119 "add_node_lock");
120 cursorImproveLock = ImageProvider.getCursor("crosshair", "lock");
121 readPreferences();
122 }
123
124 // -------------------------------------------------------------------------
125 // Mode methods
126 // -------------------------------------------------------------------------
127 @Override
128 public void enterMode() {
129 if (!isEnabled()) {
130 return;
131 }
132 super.enterMode();
133 readPreferences();
134
135 mv = Main.map.mapView;
136 mousePos = null;
137 oldModeHelpText = "";
138
139 if (getLayerManager().getEditDataSet() == null) {
140 return;
141 }
142
143 updateStateByCurrentSelection();
144
145 Main.map.mapView.addMouseListener(this);
146 Main.map.mapView.addMouseMotionListener(this);
147 Main.map.mapView.addTemporaryLayer(temporaryLayer);
148 DataSet.addSelectionListener(this);
149
150 Main.map.keyDetector.addModifierListener(this);
151 }
152
153 @Override
154 protected void readPreferences() {
155 guideColor = new ColorProperty(marktr("improve way accuracy helper line"), (Color) null).get();
156 if (guideColor == null)
157 guideColor = PaintColors.HIGHLIGHT.get();
158
159 selectTargetWayStroke = GuiHelper.getCustomizedStroke(Main.pref.get("improvewayaccuracy.stroke.select-target", "2"));
160 moveNodeStroke = GuiHelper.getCustomizedStroke(Main.pref.get("improvewayaccuracy.stroke.move-node", "1 6"));
161 moveNodeIntersectingStroke = GuiHelper.getCustomizedStroke(Main.pref.get("improvewayaccuracy.stroke.move-node-intersecting", "1 2 6"));
162 addNodeStroke = GuiHelper.getCustomizedStroke(Main.pref.get("improvewayaccuracy.stroke.add-node", "1"));
163 deleteNodeStroke = GuiHelper.getCustomizedStroke(Main.pref.get("improvewayaccuracy.stroke.delete-node", "1"));
164 dotSize = Main.pref.getInteger("improvewayaccuracy.dot-size", 6);
165 }
166
167 @Override
168 public void exitMode() {
169 super.exitMode();
170
171 Main.map.mapView.removeMouseListener(this);
172 Main.map.mapView.removeMouseMotionListener(this);
173 Main.map.mapView.removeTemporaryLayer(temporaryLayer);
174 DataSet.removeSelectionListener(this);
175
176 Main.map.keyDetector.removeModifierListener(this);
177 temporaryLayer.invalidate();
178 }
179
180 @Override
181 protected void updateStatusLine() {
182 String newModeHelpText = getModeHelpText();
183 if (!newModeHelpText.equals(oldModeHelpText)) {
184 oldModeHelpText = newModeHelpText;
185 Main.map.statusLine.setHelpText(newModeHelpText);
186 Main.map.statusLine.repaint();
187 }
188 }
189
190 @Override
191 public String getModeHelpText() {
192 if (state == State.selecting) {
193 if (targetWay != null) {
194 return tr("Click on the way to start improving its shape.");
195 } else {
196 return tr("Select a way that you want to make more accurate.");
197 }
198 } else {
199 if (ctrl) {
200 return tr("Click to add a new node. Release Ctrl to move existing nodes or hold Alt to delete.");
201 } else if (alt) {
202 return tr("Click to delete the highlighted node. Release Alt to move existing nodes or hold Ctrl to add new nodes.");
203 } else {
204 return tr("Click to move the highlighted node. Hold Ctrl to add new nodes, or Alt to delete.");
205 }
206 }
207 }
208
209 @Override
210 public boolean layerIsSupported(Layer l) {
211 return l instanceof OsmDataLayer;
212 }
213
214 @Override
215 protected void updateEnabledState() {
216 setEnabled(getLayerManager().getEditLayer() != null);
217 }
218
219 // -------------------------------------------------------------------------
220 // MapViewPaintable methods
221 // -------------------------------------------------------------------------
222 /**
223 * Redraws temporary layer. Highlights targetWay in select mode. Draws
224 * preview lines in improve mode and highlights the candidateNode
225 * @param g The graphics
226 * @param mv The map view
227 * @param bbox The bounding box
228 */
229 public void paint(Graphics2D g, MapView mv, Bounds bbox) {
230 if (mousePos == null) {
231 return;
232 }
233
234 g.setColor(guideColor);
235
236 if (state == State.selecting && targetWay != null) {
237 // Highlighting the targetWay in Selecting state
238 // Non-native highlighting is used, because sometimes highlighted
239 // segments are covered with others, which is bad.
240 g.setStroke(selectTargetWayStroke);
241
242 List<Node> nodes = targetWay.getNodes();
243
244 MapPath2D b = new MapPath2D();
245 Point p0 = mv.getPoint(nodes.get(0));
246 Point pn;
247 b.moveTo(p0.x, p0.y);
248
249 for (Node n : nodes) {
250 pn = mv.getPoint(n);
251 b.lineTo(pn.x, pn.y);
252 }
253 if (targetWay.isClosed()) {
254 b.lineTo(p0.x, p0.y);
255 }
256
257 g.draw(b);
258
259 } else if (state == State.improving) {
260 // Drawing preview lines and highlighting the node
261 // that is going to be moved.
262 // Non-native highlighting is used here as well.
263
264 // Finding endpoints
265 Point p1 = null, p2 = null;
266 if (ctrl && candidateSegment != null) {
267 g.setStroke(addNodeStroke);
268 p1 = mv.getPoint(candidateSegment.getFirstNode());
269 p2 = mv.getPoint(candidateSegment.getSecondNode());
270 } else if (!alt && !ctrl && candidateNode != null) {
271 g.setStroke(moveNodeStroke);
272 List<Pair<Node, Node>> wpps = targetWay.getNodePairs(false);
273 for (Pair<Node, Node> wpp : wpps) {
274 if (wpp.a == candidateNode) {
275 p1 = mv.getPoint(wpp.b);
276 }
277 if (wpp.b == candidateNode) {
278 p2 = mv.getPoint(wpp.a);
279 }
280 if (p1 != null && p2 != null) {
281 break;
282 }
283 }
284 } else if (alt && !ctrl && candidateNode != null) {
285 g.setStroke(deleteNodeStroke);
286 List<Node> nodes = targetWay.getNodes();
287 int index = nodes.indexOf(candidateNode);
288
289 // Only draw line if node is not first and/or last
290 if (index != 0 && index != (nodes.size() - 1)) {
291 p1 = mv.getPoint(nodes.get(index - 1));
292 p2 = mv.getPoint(nodes.get(index + 1));
293 } else if (targetWay.isClosed()) {
294 p1 = mv.getPoint(targetWay.getNode(1));
295 p2 = mv.getPoint(targetWay.getNode(nodes.size() - 2));
296 }
297 // TODO: indicate what part that will be deleted? (for end nodes)
298 }
299
300
301 // Drawing preview lines
302 MapPath2D b = new MapPath2D();
303 if (alt && !ctrl) {
304 // In delete mode
305 if (p1 != null && p2 != null) {
306 b.moveTo(p1.x, p1.y);
307 b.lineTo(p2.x, p2.y);
308 }
309 } else {
310 // In add or move mode
311 if (p1 != null) {
312 b.moveTo(mousePos.x, mousePos.y);
313 b.lineTo(p1.x, p1.y);
314 }
315 if (p2 != null) {
316 b.moveTo(mousePos.x, mousePos.y);
317 b.lineTo(p2.x, p2.y);
318 }
319 }
320 g.draw(b);
321
322 // Highlighting candidateNode
323 if (candidateNode != null) {
324 p1 = mv.getPoint(candidateNode);
325 g.fillRect(p1.x - dotSize/2, p1.y - dotSize/2, dotSize, dotSize);
326 }
327
328 if (!alt && !ctrl && candidateNode != null) {
329 b.reset();
330 drawIntersectingWayHelperLines(mv, b);
331 g.setStroke(moveNodeIntersectingStroke);
332 g.draw(b);
333 }
334
335 }
336 }
337
338 protected void drawIntersectingWayHelperLines(MapView mv, MapPath2D b) {
339 for (final OsmPrimitive referrer : candidateNode.getReferrers()) {
340 if (!(referrer instanceof Way) || targetWay.equals(referrer)) {
341 continue;
342 }
343 final List<Node> nodes = ((Way) referrer).getNodes();
344 for (int i = 0; i < nodes.size(); i++) {
345 if (!candidateNode.equals(nodes.get(i))) {
346 continue;
347 }
348 if (i > 0) {
349 final MapViewPoint p = mv.getState().getPointFor(nodes.get(i - 1));
350 b.moveTo(mousePos.x, mousePos.y);
351 b.lineTo(p);
352 }
353 if (i < nodes.size() - 1) {
354 final MapViewPoint p = mv.getState().getPointFor(nodes.get(i + 1));
355 b.moveTo(mousePos.x, mousePos.y);
356 b.lineTo(p);
357 }
358 }
359 }
360 }
361
362 // -------------------------------------------------------------------------
363 // Event handlers
364 // -------------------------------------------------------------------------
365 @Override
366 public void modifiersChanged(int modifiers) {
367 if (!Main.isDisplayingMapView() || !Main.map.mapView.isActiveLayerDrawable()) {
368 return;
369 }
370 updateKeyModifiers(modifiers);
371 updateCursorDependentObjectsIfNeeded();
372 updateCursor();
373 updateStatusLine();
374 temporaryLayer.invalidate();
375 }
376
377 @Override
378 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
379 if (selectionChangedBlocked) {
380 return;
381 }
382 updateStateByCurrentSelection();
383 }
384
385 @Override
386 public void mouseDragged(MouseEvent e) {
387 dragging = true;
388 mouseMoved(e);
389 }
390
391 @Override
392 public void mouseMoved(MouseEvent e) {
393 if (!isEnabled()) {
394 return;
395 }
396
397 mousePos = e.getPoint();
398
399 updateKeyModifiers(e);
400 updateCursorDependentObjectsIfNeeded();
401 updateCursor();
402 updateStatusLine();
403 temporaryLayer.invalidate();
404 }
405
406 @Override
407 public void mouseReleased(MouseEvent e) {
408 dragging = false;
409 if (!isEnabled() || e.getButton() != MouseEvent.BUTTON1) {
410 return;
411 }
412
413 updateKeyModifiers(e);
414 mousePos = e.getPoint();
415
416 if (state == State.selecting) {
417 if (targetWay != null) {
418 getLayerManager().getEditDataSet().setSelected(targetWay.getPrimitiveId());
419 updateStateByCurrentSelection();
420 }
421 } else if (state == State.improving && mousePos != null) {
422 // Checking if the new coordinate is outside of the world
423 if (mv.getLatLon(mousePos.x, mousePos.y).isOutSideWorld()) {
424 JOptionPane.showMessageDialog(Main.parent,
425 tr("Cannot add a node outside of the world."),
426 tr("Warning"), JOptionPane.WARNING_MESSAGE);
427 return;
428 }
429
430 if (ctrl && !alt && candidateSegment != null) {
431 // Adding a new node to the highlighted segment
432 // Important: If there are other ways containing the same
433 // segment, a node must added to all of that ways.
434 Collection<Command> virtualCmds = new LinkedList<>();
435
436 // Creating a new node
437 Node virtualNode = new Node(mv.getEastNorth(mousePos.x,
438 mousePos.y));
439 virtualCmds.add(new AddCommand(virtualNode));
440
441 // Looking for candidateSegment copies in ways that are
442 // referenced
443 // by candidateSegment nodes
444 List<Way> firstNodeWays = OsmPrimitive.getFilteredList(
445 candidateSegment.getFirstNode().getReferrers(),
446 Way.class);
447 List<Way> secondNodeWays = OsmPrimitive.getFilteredList(
448 candidateSegment.getFirstNode().getReferrers(),
449 Way.class);
450
451 Collection<WaySegment> virtualSegments = new LinkedList<>();
452 for (Way w : firstNodeWays) {
453 List<Pair<Node, Node>> wpps = w.getNodePairs(true);
454 for (Way w2 : secondNodeWays) {
455 if (!w.equals(w2)) {
456 continue;
457 }
458 // A way is referenced in both nodes.
459 // Checking if there is such segment
460 int i = -1;
461 for (Pair<Node, Node> wpp : wpps) {
462 ++i;
463 boolean ab = wpp.a.equals(candidateSegment.getFirstNode())
464 && wpp.b.equals(candidateSegment.getSecondNode());
465 boolean ba = wpp.b.equals(candidateSegment.getFirstNode())
466 && wpp.a.equals(candidateSegment.getSecondNode());
467 if (ab || ba) {
468 virtualSegments.add(new WaySegment(w, i));
469 }
470 }
471 }
472 }
473
474 // Adding the node to all segments found
475 for (WaySegment virtualSegment : virtualSegments) {
476 Way w = virtualSegment.way;
477 Way wnew = new Way(w);
478 wnew.addNode(virtualSegment.lowerIndex + 1, virtualNode);
479 virtualCmds.add(new ChangeCommand(w, wnew));
480 }
481
482 // Finishing the sequence command
483 String text = trn("Add a new node to way",
484 "Add a new node to {0} ways",
485 virtualSegments.size(), virtualSegments.size());
486
487 Main.main.undoRedo.add(new SequenceCommand(text, virtualCmds));
488
489 } else if (alt && !ctrl && candidateNode != null) {
490 // Deleting the highlighted node
491
492 //check to see if node is in use by more than one object
493 List<OsmPrimitive> referrers = candidateNode.getReferrers();
494 List<Way> ways = OsmPrimitive.getFilteredList(referrers, Way.class);
495 if (referrers.size() != 1 || ways.size() != 1) {
496 // detach node from way
497 final Way newWay = new Way(targetWay);
498 final List<Node> nodes = newWay.getNodes();
499 nodes.remove(candidateNode);
500 newWay.setNodes(nodes);
501 Main.main.undoRedo.add(new ChangeCommand(targetWay, newWay));
502 } else if (candidateNode.isTagged()) {
503 JOptionPane.showMessageDialog(Main.parent,
504 tr("Cannot delete node that has tags"),
505 tr("Error"), JOptionPane.ERROR_MESSAGE);
506 } else {
507 List<Node> nodeList = new ArrayList<>();
508 nodeList.add(candidateNode);
509 Command deleteCmd = DeleteCommand.delete(getLayerManager().getEditLayer(), nodeList, true);
510 if (deleteCmd != null) {
511 Main.main.undoRedo.add(deleteCmd);
512 }
513 }
514
515
516 } else if (candidateNode != null) {
517 // Moving the highlighted node
518 EastNorth nodeEN = candidateNode.getEastNorth();
519 EastNorth cursorEN = mv.getEastNorth(mousePos.x, mousePos.y);
520
521 Main.main.undoRedo.add(new MoveCommand(candidateNode, cursorEN.east() - nodeEN.east(), cursorEN.north() - nodeEN.north()));
522 }
523 }
524
525 mousePos = null;
526 updateCursor();
527 updateStatusLine();
528 temporaryLayer.invalidate();
529 }
530
531 @Override
532 public void mouseExited(MouseEvent e) {
533 if (!isEnabled()) {
534 return;
535 }
536
537 if (!dragging) {
538 mousePos = null;
539 }
540 temporaryLayer.invalidate();
541 }
542
543 // -------------------------------------------------------------------------
544 // Custom methods
545 // -------------------------------------------------------------------------
546 /**
547 * Sets new cursor depending on state, mouse position
548 */
549 private void updateCursor() {
550 if (!isEnabled()) {
551 mv.setNewCursor(null, this);
552 return;
553 }
554
555 if (state == State.selecting) {
556 mv.setNewCursor(targetWay == null ? cursorSelect
557 : cursorSelectHover, this);
558 } else if (state == State.improving) {
559 if (alt && !ctrl) {
560 mv.setNewCursor(cursorImproveDelete, this);
561 } else if (shift || dragging) {
562 if (ctrl) {
563 mv.setNewCursor(cursorImproveAddLock, this);
564 } else {
565 mv.setNewCursor(cursorImproveLock, this);
566 }
567 } else if (ctrl && !alt) {
568 mv.setNewCursor(cursorImproveAdd, this);
569 } else {
570 mv.setNewCursor(cursorImprove, this);
571 }
572 }
573 }
574
575 /**
576 * Updates these objects under cursor: targetWay, candidateNode,
577 * candidateSegment
578 */
579 public void updateCursorDependentObjectsIfNeeded() {
580 if (state == State.improving && (shift || dragging)
581 && !(candidateNode == null && candidateSegment == null)) {
582 return;
583 }
584
585 if (mousePos == null) {
586 candidateNode = null;
587 candidateSegment = null;
588 return;
589 }
590
591 if (state == State.selecting) {
592 targetWay = ImproveWayAccuracyHelper.findWay(mv, mousePos);
593 } else if (state == State.improving) {
594 if (ctrl && !alt) {
595 candidateSegment = ImproveWayAccuracyHelper.findCandidateSegment(mv,
596 targetWay, mousePos);
597 candidateNode = null;
598 } else {
599 candidateNode = ImproveWayAccuracyHelper.findCandidateNode(mv,
600 targetWay, mousePos);
601 candidateSegment = null;
602 }
603 }
604 }
605
606 /**
607 * Switches to Selecting state
608 */
609 public void startSelecting() {
610 state = State.selecting;
611
612 targetWay = null;
613
614 temporaryLayer.invalidate();
615 updateStatusLine();
616 }
617
618 /**
619 * Switches to Improving state
620 *
621 * @param targetWay Way that is going to be improved
622 */
623 public void startImproving(Way targetWay) {
624 state = State.improving;
625
626 DataSet ds = getLayerManager().getEditDataSet();
627 Collection<OsmPrimitive> currentSelection = ds.getSelected();
628 if (currentSelection.size() != 1
629 || !currentSelection.iterator().next().equals(targetWay)) {
630 selectionChangedBlocked = true;
631 ds.clearSelection();
632 ds.setSelected(targetWay.getPrimitiveId());
633 selectionChangedBlocked = false;
634 }
635
636 this.targetWay = targetWay;
637 this.candidateNode = null;
638 this.candidateSegment = null;
639
640 temporaryLayer.invalidate();
641 updateStatusLine();
642 }
643
644 /**
645 * Updates the state according to the current selection. Goes to Improve
646 * state if a single way or node is selected. Extracts a way by a node in
647 * the second case.
648 *
649 */
650 private void updateStateByCurrentSelection() {
651 final List<Node> nodeList = new ArrayList<>();
652 final List<Way> wayList = new ArrayList<>();
653 final Collection<OsmPrimitive> sel = getLayerManager().getEditDataSet().getSelected();
654
655 // Collecting nodes and ways from the selection
656 for (OsmPrimitive p : sel) {
657 if (p instanceof Way) {
658 wayList.add((Way) p);
659 }
660 if (p instanceof Node) {
661 nodeList.add((Node) p);
662 }
663 }
664
665 if (wayList.size() == 1) {
666 // Starting improving the single selected way
667 startImproving(wayList.get(0));
668 return;
669 } else if (nodeList.size() == 1) {
670 // Starting improving the only way of the single selected node
671 List<OsmPrimitive> r = nodeList.get(0).getReferrers();
672 if (r.size() == 1 && (r.get(0) instanceof Way)) {
673 startImproving((Way) r.get(0));
674 return;
675 }
676 }
677
678 // Starting selecting by default
679 startSelecting();
680 }
681}
Note: See TracBrowser for help on using the repository browser.