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