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