1 package net.systemeD.potlatch2.controller {
4 import flash.display.DisplayObject;
5 import flash.ui.Keyboard;
6 import net.systemeD.potlatch2.EditController;
7 import net.systemeD.halcyon.connection.*;
8 import net.systemeD.halcyon.connection.actions.*;
9 import net.systemeD.halcyon.Elastic;
10 import net.systemeD.halcyon.MapPaint;
12 public class DrawWay extends SelectedWay {
13 private var elastic:Elastic;
14 private var editEnd:Boolean; // if true, we're drawing from node[n-1], else "backwards" from node[0]
15 private var leaveNodeSelected:Boolean;
16 private var lastClick:Entity=null;
17 private var lastClickTime:Date;
18 private var hoverEntity:Entity; // keep track of the currently rolled-over object, because
19 // Flash can fire a mouseDown from the map even if you
20 // haven't rolled out of the way
22 public function DrawWay(way:Way, editEnd:Boolean, leaveNodeSelected:Boolean) {
24 this.editEnd = editEnd;
25 this.leaveNodeSelected = leaveNodeSelected;
27 // drawing new way, so keep track of click in case creating a POI
28 lastClick=way.getNode(0);
29 lastClickTime=new Date();
33 override public function processMouseEvent(event:MouseEvent, entity:Entity):ControllerState {
36 var paint:MapPaint = getMapPaint(DisplayObject(event.target));
37 var isBackground:Boolean = paint && paint.isBackground;
39 if (entity == null && hoverEntity) { entity=hoverEntity; }
40 var focus:Entity = getTopLevelFocusEntity(entity);
42 if ( event.type == MouseEvent.MOUSE_UP ) {
43 controller.map.mouseUpHandler(); // in case you're still in the drag-tolerance zone, and mouse up over something.
44 if ( entity == null || isBackground ) { // didn't hit anything: extend the way by one node.
45 node = createAndAddNode(event, MainUndoStack.getGlobalStack().addAction);
46 controller.map.setHighlight(node, { selectedway: true });
47 controller.map.setPurgable([node], false);
50 controller.updateSelectionUIWithoutTagChange();
51 } else if ( entity is Node ) {
52 if (entity==lastClick && (new Date().getTime()-lastClickTime.getTime())<1000) {
53 if (Way(firstSelected).length==1 && Way(firstSelected).getNode(0).parentWays.length==1) {
54 // Actually the user double-clicked to make a new node, they didn't want to draw a way at all.
56 MainUndoStack.getGlobalStack().undo(); // undo the BeginWayAction that (presumably?) just happened
58 var newPoiAction:CreatePOIAction = new CreatePOIAction(
60 controller.map.coord2lat(event.localY),
61 controller.map.coord2lon(event.localX));
62 MainUndoStack.getGlobalStack().addAction(newPoiAction);
63 return new SelectedPOINode(newPoiAction.getNode());
64 } else if (Way(firstSelected).length==1) {
65 // It's not a poi, but they've double-clicked or clicked-twice the first node - do nothing
68 // double-click at end of way
71 } else if (entity==lastClick) {
72 // clicked slowly on the end node - do nothing
75 // hit a node, add it to this way and carry on
76 appendNode(entity as Node, MainUndoStack.getGlobalStack().addAction);
78 controller.map.setHighlightOnNodes(focus as Way, { hoverway: false });
80 controller.map.setHighlight(entity, { selectedway: true });
81 resetElastic(entity as Node);
83 if (Way(firstSelected).getNode(0)==Way(firstSelected).getLastNode()) {
84 // the node just hit completes a loop, so stop drawing.
85 return new SelectedWay(firstSelected as Way);
88 } else if ( entity is Way ) {
89 if (entity==firstSelected) {
90 // add junction node - self-intersecting way
91 var lat:Number = controller.map.coord2lat(event.localY);
92 var lon:Number = controller.map.coord2lon(event.localX);
93 var undo:CompositeUndoableAction = new CompositeUndoableAction("Insert node");
94 node = controller.connection.createNode({}, lat, lon, undo.push);
95 Way(firstSelected).insertNodeAtClosestPosition(node, true, undo.push);
96 appendNode(node,undo.push);
97 MainUndoStack.getGlobalStack().addAction(undo);
99 // add junction node - another way
100 var jnct:CompositeUndoableAction = new CompositeUndoableAction("Junction Node");
101 node = createAndAddNode(event, jnct.push);
102 Way(entity).insertNodeAtClosestPosition(node, true, jnct.push);
103 MainUndoStack.getGlobalStack().addAction(jnct);
104 controller.map.setHighlight(node, { selectedway: true });
105 controller.map.setPurgable([node], false);
109 controller.map.setHighlightOnNodes(entity as Way, { hoverway: false });
110 controller.map.setHighlightOnNodes(firstSelected as Way, { selectedway: true });
112 lastClickTime=new Date();
113 } else if ( event.type == MouseEvent.MOUSE_MOVE && elastic ) {
114 // mouse is roaming around freely
116 controller.map.coord2lon(event.localX),
117 controller.map.coord2latp(event.localY));
119 } else if ( event.type == MouseEvent.ROLL_OVER && !isBackground ) {
120 // mouse has floated over something
121 if (focus is Way && focus!=firstSelected) {
122 // floating over another way, highlight its nodes
124 controller.map.setHighlightOnNodes(focus as Way, { hoverway: true });
126 // set cursor depending on whether we're floating over the start of this way,
127 // another random node, a possible junction...
128 if (entity is Node && focus is Way && Way(focus).endsWith(Node(entity))) {
129 if (focus==firstSelected) { controller.setCursor(controller.pen_so); }
130 else { controller.setCursor(controller.pen_o); }
131 } else if (entity is Node) {
132 controller.setCursor(controller.pen_x);
134 controller.setCursor(controller.pen_plus);
136 } else if ( event.type == MouseEvent.MOUSE_OUT && !isBackground ) {
137 if (focus is Way && entity!=firstSelected) {
139 controller.map.setHighlightOnNodes(focus as Way, { hoverway: false });
140 // ** We could do with an optional way of calling WayUI.redraw to only do the nodes, which would be a
141 // useful optimisation.
143 controller.setCursor(controller.pen);
149 protected function resetElastic(node:Node):void {
150 elastic.start = new Point(node.lon, node.latp);
151 elastic.end = new Point(controller.map.coord2lon(controller.map.mouseX),
152 controller.map.coord2latp(controller.map.mouseY));
155 /* Fix up the elastic after a WayNode event - e.g. triggered by undo */
156 private function fixElastic(event:Event):void {
157 if (firstSelected == null) return;
160 node = Way(firstSelected).getLastNode();
162 node = Way(firstSelected).getNode(0);
164 if (node) { //maybe selectedWay doesn't have any nodes left
165 elastic.start = new Point(node.lon, node.latp);
169 override public function processKeyboardEvent(event:KeyboardEvent):ControllerState {
170 switch (event.keyCode) {
171 case Keyboard.ENTER: return keyExitDrawing();
172 case Keyboard.ESCAPE: return keyExitDrawing();
173 case Keyboard.DELETE:
174 case Keyboard.BACKSPACE:
175 case 189: /* minus */ return backspaceNode(MainUndoStack.getGlobalStack().addAction);
176 case 82: /* R */ repeatTags(firstSelected); return this;
177 case 70: /* F */ followWay(); return this;
179 var cs:ControllerState = sharedKeyboardEvents(event);
180 return cs ? cs : this;
184 protected function keyExitDrawing():ControllerState {
185 var cs:ControllerState=stopDrawing();
186 if (selectedWay.length==1) {
187 if (MainUndoStack.getGlobalStack().undoIfAction(BeginWayAction)) {
188 return new NoSelection();
195 protected function stopDrawing():ControllerState {
197 controller.map.setHighlightOnNodes(hoverEntity as Way, { hoverway: false });
201 if ( leaveNodeSelected ) {
202 return new SelectedWayNode(firstSelected as Way, editEnd ? Way(firstSelected).length-1 : 0);
204 return new SelectedWay(firstSelected as Way);
208 public function createAndAddNode(event:MouseEvent, performAction:Function):Node {
209 var undo:CompositeUndoableAction = new CompositeUndoableAction("Add node");
211 var lat:Number = controller.map.coord2lat(event.localY);
212 var lon:Number = controller.map.coord2lon(event.localX);
213 var node:Node = controller.connection.createNode({}, lat, lon, undo.push);
214 appendNode(node, undo.push);
220 protected function appendNode(node:Node, performAction:Function):void {
222 Way(firstSelected).appendNode(node, performAction);
224 Way(firstSelected).insertNode(0, node, performAction);
227 protected function backspaceNode(performAction:Function):ControllerState {
228 if (selectedWay.length==1) return keyExitDrawing();
231 var undo:CompositeUndoableAction = new CompositeUndoableAction("Remove node");
233 var state:ControllerState;
236 node=Way(firstSelected).getLastNode();
237 Way(firstSelected).removeNodeByIndex(Way(firstSelected).length-1, undo.push);
238 newDraw=Way(firstSelected).length-2;
240 node=Way(firstSelected).getNode(0);
241 Way(firstSelected).removeNodeByIndex(0, undo.push);
244 // Only actually delete the node if it has no other tags, and is not part of other ways (or part of this way twice)
245 if (node.numParentWays==1 && Way(firstSelected).hasOnceOnly(node) && !node.hasInterestingTags()) {
246 controller.map.setPurgable([node], true);
247 controller.connection.unregisterPOI(node);
248 node.remove(undo.push);
251 if (newDraw>=0 && newDraw<=Way(firstSelected).length-2) {
252 var mouse:Point = new Point(Way(firstSelected).getNode(newDraw).lon, Way(firstSelected).getNode(newDraw).latp);
253 elastic.start = mouse;
256 Way(firstSelected).remove(undo.push);
257 state = new NoSelection();
262 if(!node.isDeleted()) { // i.e. was junction with another way (or is now POI)
263 controller.map.setHighlight(node, {selectedway: false});
268 /** Extends the current way by "following" an existing way, after the user has already selected two nodes in a row.
269 If drawing way has at least two nodes, and both belong to another way, and those ways are the same,
270 then find the next node, add that node, update screen and scroll the new node into shot if necessary.
271 TODO: add a bit of feedback (FloatingAlert?) when following can't be carried out for some reason. */
272 protected function followWay():void {
275 if (Way(firstSelected).length < 2) return;
278 curnode = Way(firstSelected).getLastNode();
279 prevnode = Way(firstSelected).getNode(Way(firstSelected).length-2);
281 curnode = Way(firstSelected).getNode(0);
282 prevnode = Way(firstSelected).getNode(1);
284 if (curnode.numParentWays <2 || prevnode.numParentWays <2) return;
287 for each (var way:Way in curnode.parentWays) {
288 if (way!=firstSelected && prevnode.hasParent(way))
289 followedWay = way; // FIXME: could be smarter when there's more than one candidate
291 if (!followedWay) return;
294 if (followedWay.getNextNode(prevnode) == curnode) {
295 nextNode = followedWay.getNextNode(curnode);
296 } else if (followedWay.getNextNode(curnode) == prevnode){
297 nextNode = followedWay.getPrevNode(curnode);
298 } else if (followedWay.indexOfNode(curnode) > followedWay.indexOfNode(prevnode)) {
299 // The two nodes selected aren't actually consecutive. Make a half-hearted
300 // guess at which way to follow. Will be "incorrect" if the join in the loop
301 // is between the two points.
302 nextNode = followedWay.getNextNode(curnode);
304 nextNode = followedWay.getPrevNode(curnode);
306 if (!nextNode) return;
307 if (nextNode.hasParent(firstSelected) && !(firstSelected as Way).hasOnceOnly(curnode)) return;
309 appendNode(nextNode as Node, MainUndoStack.getGlobalStack().addAction);
310 resetElastic(nextNode as Node);
312 controller.map.setHighlight(nextNode, { selectedway: true });
314 // recentre the map if the new lat/lon is offscreen
315 controller.map.scrollIfNeeded(nextNode.lat,nextNode.lon);
318 override public function enterState():void {
321 Way(firstSelected).addEventListener(Connection.WAY_NODE_REMOVED, fixElastic);
322 Way(firstSelected).addEventListener(Connection.WAY_NODE_ADDED, fixElastic);
324 var node:Node = Way(firstSelected).getNode(editEnd ? Way(firstSelected).length-1 : 0);
325 var start:Point = new Point(node.lon, node.latp);
326 elastic = new Elastic(controller.map, start, start);
327 controller.setCursor(controller.pen);
329 override public function exitState(newState:ControllerState):void {
330 Way(firstSelected).removeEventListener(Connection.WAY_NODE_REMOVED, fixElastic);
331 Way(firstSelected).removeEventListener(Connection.WAY_NODE_ADDED, fixElastic);
333 super.exitState(newState);
334 controller.setCursor(null);
335 elastic.removeSprites();
338 override public function toString():String {