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