ADD getNextNode(), getPrevNode() to net/systemeD/halcyon/connection/Way.as. These...
[potlatch2.git] / net / systemeD / potlatch2 / controller / DrawWay.as
1 package net.systemeD.potlatch2.controller {
2         import flash.events.*;
3         import flash.geom.*;
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.Globals;
11         import net.systemeD.halcyon.MapPaint;
12
13         public class DrawWay extends SelectedWay {
14                 private var elastic:Elastic;
15                 private var editEnd:Boolean;            // if true, we're drawing from node[n-1], else "backwards" from node[0] 
16                 private var leaveNodeSelected:Boolean;
17                 private var lastClick:Entity=null;
18                 private var lastClickTime:Date;
19                 private var hoverEntity:Entity;                 // keep track of the currently rolled-over object, because
20                                                                                                 // Flash can fire a mouseDown from the map even if you
21                                                                                                 // haven't rolled out of the way
22                 
23                 public function DrawWay(way:Way, editEnd:Boolean, leaveNodeSelected:Boolean) {
24                         super(way);
25                         this.editEnd = editEnd;
26                         this.leaveNodeSelected = leaveNodeSelected;
27                         if (way.length==1 && way.getNode(0).parentWays.length==1) {
28                                 // drawing new way, so keep track of click in case creating a POI
29                                 lastClick=way.getNode(0);
30                                 lastClickTime=new Date();
31                         }
32                 }
33                 
34                 override public function processMouseEvent(event:MouseEvent, entity:Entity):ControllerState {
35                         var mouse:Point;
36                         var node:Node;
37                         var paint:MapPaint = getMapPaint(DisplayObject(event.target));
38                         var isBackground:Boolean = paint && paint.isBackground;
39
40                         if (entity == null && hoverEntity) { entity=hoverEntity; }
41                         var focus:Entity = getTopLevelFocusEntity(entity);
42
43                         if ( event.type == MouseEvent.MOUSE_UP ) {
44                 controller.map.mouseUpHandler(); // in case you're still in the drag-tolerance zone, and mouse up over something.
45                                 if ( entity == null || isBackground ) { // didn't hit anything: extend the way by one node.
46                                         node = createAndAddNode(event, MainUndoStack.getGlobalStack().addAction);
47                     controller.map.setHighlight(node, { selectedway: true });
48                     controller.map.setPurgable([node], false);
49                                         resetElastic(node);
50                                         lastClick=node;
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.
55                             stopDrawing();
56                             MainUndoStack.getGlobalStack().undo(); // undo the BeginWayAction that (presumably?) just happened
57                             
58                             var newPoiAction:CreatePOIAction = new CreatePOIAction(
59                                                                 {},
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 {
65                                                         // double-click at end of way
66                                                         return stopDrawing();
67                                                 }
68                     } else if (entity==lastClick) {
69                         // clicked slowly on the end node - do nothing
70                         return this;
71                                         } else {
72                                                 // hit a node, add it to this way and carry on
73                                                 appendNode(entity as Node, MainUndoStack.getGlobalStack().addAction);
74                                                 if (focus is Way) {
75                           controller.map.setHighlightOnNodes(focus as Way, { hoverway: false });
76                         }
77                                                 controller.map.setHighlight(entity, { selectedway: true });
78                                                 resetElastic(entity as Node);
79                                                 lastClick=entity;
80                                                 if (Way(firstSelected).getNode(0)==Way(firstSelected).getLastNode()) {
81                                                         // the node just hit completes a loop, so stop drawing.
82                                                         return new SelectedWay(firstSelected as Way);
83                                                 }
84                                         }
85                                 } else if ( entity is Way ) {
86                                         if (entity==firstSelected) {
87                                                 // add junction node - self-intersecting way
88                                     var lat:Number = controller.map.coord2lat(event.localY);
89                                     var lon:Number = controller.map.coord2lon(event.localX);
90                                     var undo:CompositeUndoableAction = new CompositeUndoableAction("Insert node");
91                                     node = controller.connection.createNode({}, lat, lon, undo.push);
92                                     Way(firstSelected).insertNodeAtClosestPosition(node, true, undo.push);
93                                                 appendNode(node,undo.push);
94                                     MainUndoStack.getGlobalStack().addAction(undo);
95                                         } else {
96                         // add junction node - another way
97                         var jnct:CompositeUndoableAction = new CompositeUndoableAction("Junction Node");
98                         node = createAndAddNode(event, jnct.push);
99                         Way(entity).insertNodeAtClosestPosition(node, true, jnct.push);
100                         MainUndoStack.getGlobalStack().addAction(jnct);
101                         controller.map.setHighlight(node, { selectedway: true });
102                         controller.map.setPurgable([node], false);
103                                         }
104                                         resetElastic(node);
105                                         lastClick=node;
106                                         controller.map.setHighlightOnNodes(entity as Way, { hoverway: false });
107                                         controller.map.setHighlightOnNodes(firstSelected as Way, { selectedway: true });
108                                 }
109                                 lastClickTime=new Date();
110                         } else if ( event.type == MouseEvent.MOUSE_MOVE && elastic ) {
111                                 // mouse is roaming around freely
112                                 mouse = new Point(
113                                                   controller.map.coord2lon(event.localX),
114                                                   controller.map.coord2latp(event.localY));
115                                 elastic.end = mouse;
116                         } else if ( event.type == MouseEvent.ROLL_OVER && !isBackground ) {
117                                 // mouse has floated over something
118                                 if (focus is Way && focus!=firstSelected) {
119                                         // floating over another way, highlight its nodes
120                                         hoverEntity=focus;
121                                         controller.map.setHighlightOnNodes(focus as Way, { hoverway: true });
122                                 }
123                                 // set cursor depending on whether we're floating over the start of this way, 
124                                 // another random node, a possible junction...
125                                 if (entity is Node && focus is Way && Way(focus).endsWith(Node(entity))) {
126                                         if (focus==firstSelected) { controller.setCursor(controller.pen_so); }
127                                                              else { controller.setCursor(controller.pen_o); }
128                                 } else if (entity is Node) {
129                                         controller.setCursor(controller.pen_x);
130                                 } else {
131                                         controller.setCursor(controller.pen_plus);
132                                 }
133                         } else if ( event.type == MouseEvent.MOUSE_OUT && !isBackground ) {
134                                 if (focus is Way && entity!=firstSelected) {
135                                         hoverEntity=null;
136                                         controller.map.setHighlightOnNodes(focus as Way, { hoverway: false });
137                                         // ** We could do with an optional way of calling WayUI.redraw to only do the nodes, which would be a
138                                         // useful optimisation.
139                                 }
140                                 controller.setCursor(controller.pen);
141                         }
142
143                         return this;
144                 }
145                 
146                 protected function resetElastic(node:Node):void {
147                         var mouse:Point = new Point(node.lon, node.latp);
148                         elastic.start = mouse;
149                         elastic.end = mouse;
150                 }
151
152         /* Fix up the elastic after a WayNode event - e.g. triggered by undo */
153         private function fixElastic(event:Event):void {
154             if (firstSelected == null) return;
155             var node:Node;
156             if (editEnd) {
157               node = Way(firstSelected).getLastNode();
158             } else {
159               node = Way(firstSelected).getNode(0);
160             }
161             if (node) { //maybe selectedWay doesn't have any nodes left
162               elastic.start = new Point(node.lon, node.latp);
163             }
164         }
165
166                 override public function processKeyboardEvent(event:KeyboardEvent):ControllerState {
167                         switch (event.keyCode) {
168                                 case Keyboard.ENTER:                                    return keyExitDrawing();
169                                 case Keyboard.ESCAPE:                                   return keyExitDrawing();
170                                 case Keyboard.DELETE:           
171                                 case Keyboard.BACKSPACE:        
172                                 case 189: /* minus */       return backspaceNode(MainUndoStack.getGlobalStack().addAction);
173                                 case 82: /* R */            repeatTags(firstSelected); return this;
174                                 case 70: /* F */            followWay(); return this;
175                         }
176                         var cs:ControllerState = sharedKeyboardEvents(event);
177                         return cs ? cs : this;
178                         
179                 }
180                 
181                 protected function keyExitDrawing():ControllerState {
182                         var cs:ControllerState=stopDrawing();
183                         if (selectedWay.length==1) { 
184                                 if (MainUndoStack.getGlobalStack().undoIfAction(BeginWayAction)) { 
185                                         return new NoSelection();
186                                 }
187                                 return deleteWay();
188                         }
189                         return cs;
190                 }
191                 
192                 protected function stopDrawing():ControllerState {
193                         if ( hoverEntity ) {
194                                 controller.map.setHighlightOnNodes(hoverEntity as Way, { hoverway: false });
195                                 hoverEntity = null;
196                         }
197
198                         if ( leaveNodeSelected ) {
199                             return new SelectedWayNode(firstSelected as Way, editEnd ? Way(firstSelected).length-1 : 0);
200                         } else {
201                             return new SelectedWay(firstSelected as Way);
202                         }
203                 }
204
205                 public function createAndAddNode(event:MouseEvent, performAction:Function):Node {
206                     var undo:CompositeUndoableAction = new CompositeUndoableAction("Add node");
207                     
208                         var lat:Number = controller.map.coord2lat(event.localY);
209                         var lon:Number = controller.map.coord2lon(event.localX);
210                         var node:Node = controller.connection.createNode({}, lat, lon, undo.push);
211                         appendNode(node, undo.push);
212                         
213                         performAction(undo);
214                         return node;
215                 }
216                 
217                 protected function appendNode(node:Node, performAction:Function):void {
218                         if ( editEnd )
219                                 Way(firstSelected).appendNode(node, performAction);
220                         else
221                                 Way(firstSelected).insertNode(0, node, performAction);
222                 }
223                 
224                 protected function backspaceNode(performAction:Function):ControllerState {
225                         if (selectedWay.length==1) return keyExitDrawing();
226
227                         var node:Node;
228                         var undo:CompositeUndoableAction = new CompositeUndoableAction("Remove node");
229                         var newDraw:int;
230             var state:ControllerState;
231
232                         if (editEnd) {
233                                 node=Way(firstSelected).getLastNode();
234                                 Way(firstSelected).removeNodeByIndex(Way(firstSelected).length-1, undo.push);
235                                 newDraw=Way(firstSelected).length-2;
236                         } else {
237                                 node=Way(firstSelected).getNode(0);
238                                 Way(firstSelected).removeNodeByIndex(0, undo.push);
239                                 newDraw=0;
240                         }
241                         // Only actually delete the node if it has no other tags, and is not part of other ways (or part of this way twice)
242                         if (node.numParentWays==1 && Way(firstSelected).hasOnceOnly(node) && !node.hasInterestingTags()) {
243                                 controller.map.setPurgable([node], true);
244                                 controller.connection.unregisterPOI(node);
245                                 node.remove(undo.push);
246                         }
247
248                         if (newDraw>=0 && newDraw<=Way(firstSelected).length-2) {
249                                 var mouse:Point = new Point(Way(firstSelected).getNode(newDraw).lon, Way(firstSelected).getNode(newDraw).latp);
250                                 elastic.start = mouse;
251                                 state = this;
252                         } else {
253                 Way(firstSelected).remove(undo.push);
254                 state = new NoSelection();
255                         }
256
257             performAction(undo);
258
259             if(!node.isDeleted()) { // i.e. was junction with another way (or is now POI)
260               controller.map.setHighlight(node, {selectedway: false});
261             }
262             return state;
263                 }
264                 
265                 /** Extends the current way by "following" an existing way, after the user has already selected two nodes in a row. */
266                 protected function followWay() {
267                         // TODO add a bit of feedback when following can't be carried out for some reason. 
268                         // Perhaps use the new FloatingAlert that I couldn't figure out how to use 
269                         
270                   /*
271                   If drawing way has at least two nodes
272                   and both belong to another way
273                   and those ways are the same
274                   then find the next node
275                   then add that node
276                   then update screen and scroll the new node into shot if necessary */
277                   var curnode:Node;
278                   var prevnode:Node;
279                   if (Way(firstSelected).length < 2) {
280                       return;
281                   }
282                   if (editEnd) {
283                         curnode = Way(firstSelected).getLastNode();
284                         prevnode = Way(firstSelected).getNode(Way(firstSelected).length-2);
285                   } else {
286             curnode = Way(firstSelected).getNode(0);
287             prevnode = Way(firstSelected).getNode(1);
288                 
289                   }
290                   if (curnode.numParentWays <2 || prevnode.numParentWays <2) 
291                       return;
292                   var followedWay:Way = null;
293                   for (var i in curnode.parentWays) {
294                         if (curnode.parentWays[i]!=firstSelected && prevnode.hasParent(curnode.parentWays[i])) {
295                                 // Could be slightly smarter when there are more than one candidate. Perhaps could see which
296                                 // of the 2+ ways is the most colinear with the currently drawn way.
297                                 followedWay = curnode.parentWays[i];
298                         }
299                   }
300                   if (!followedWay)
301                       return;
302                   var nextNode:Node;
303           if (followedWay.getNextNode(prevnode) == curnode) {
304               nextNode = followedWay.getNextNode(curnode);
305           } else if (followedWay.getNextNode(curnode) == prevnode){
306               nextNode = followedWay.getPrevNode(curnode);
307           } else // The two nodes selected aren't actually consecutive. Make a half-hearted
308                  // guess at which way to follow. Will be "incorrect" if the join in the loop
309                  // is between the two points. 
310             if (followedWay.indexOfNode(curnode) > followedWay.indexOfNode(prevnode)) {
311               nextNode = followedWay.getNextNode(curnode);
312             } else {
313               nextNode = followedWay.getPrevNode(curnode);
314             }
315           if (!nextNode) return;
316           if (nextNode.hasParent(firstSelected))
317             return;
318           
319           appendNode(nextNode as Node, MainUndoStack.getGlobalStack().addAction);
320           resetElastic(nextNode as Node);
321           lastClick=nextNode;
322           controller.map.setHighlight(nextNode, { selectedway: true });
323           
324           // recentre the map if the new lat/lon is offscreen
325           if (nextNode.lat > controller.map.edge_t ||
326               nextNode.lat < controller.map.edge_b ||  
327               nextNode.lon < controller.map.edge_l ||
328               nextNode.lon > controller.map.edge_r) {
329                 
330             controller.map.moveMapFromLatLon(nextNode.lat, nextNode.lon);
331           }
332                 }
333                 
334                 override public function enterState():void {
335                         super.enterState();
336                         
337             Way(firstSelected).addEventListener(Connection.WAY_NODE_REMOVED, fixElastic);
338             Way(firstSelected).addEventListener(Connection.WAY_NODE_ADDED, fixElastic);
339
340                         var node:Node = Way(firstSelected).getNode(editEnd ? Way(firstSelected).length-1 : 0);
341                         var start:Point = new Point(node.lon, node.latp);
342                         elastic = new Elastic(controller.map, start, start);
343                         controller.setCursor(controller.pen);
344                         Globals.vars.root.addDebug("**** -> "+this);
345                 }
346                 override public function exitState(newState:ControllerState):void {
347             Way(firstSelected).removeEventListener(Connection.WAY_NODE_REMOVED, fixElastic);
348             Way(firstSelected).removeEventListener(Connection.WAY_NODE_ADDED, fixElastic);
349
350                         super.exitState(newState);
351                         controller.setCursor(null);
352                         elastic.removeSprites();
353                         elastic = null;
354                         Globals.vars.root.addDebug("**** <- "+this);
355                 }
356                 override public function toString():String {
357                         return "DrawWay";
358                 }
359         }
360 }