Merge remote-tracking branch 'stevage/repeat-relations-3596' into repeatrelations
[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                         var ways:Array = layer.findWaysAtPoint(event.stageX, event.stageY, selectedWay);
105                         node = createAndAddNode(event, jnct.push);
106                         for each (var w:Way in ways) { w.insertNodeAtClosestPosition(node, true, jnct.push); }
107                         MainUndoStack.getGlobalStack().addAction(jnct);
108                         layer.setHighlight(node, { selectedway: true });
109                         layer.setPurgable([node], false);
110                                         }
111                                         resetElastic(node);
112                                         lastClick=node;
113                                         layer.setHighlightOnNodes(entity as Way, { hoverway: false });
114                                         layer.setHighlightOnNodes(firstSelected as Way, { selectedway: true });
115                                 }
116                                 lastClickTime=new Date();
117                         } else if ( event.type == MouseEvent.MOUSE_MOVE && elastic ) {
118                                 // mouse is roaming around freely
119                                 mouse = new Point(
120                                                   controller.map.coord2lon(event.localX),
121                                                   controller.map.coord2latp(event.localY));
122                                 elastic.end = mouse;
123                         } else if ( event.type == MouseEvent.ROLL_OVER && !isBackground ) {
124                                 // mouse has floated over something
125                                 if (focus is Way && focus!=firstSelected) {
126                                         // floating over another way, highlight its nodes
127                                         hoverEntity=focus;
128                                         layer.setHighlightOnNodes(focus as Way, { hoverway: true });
129                                 }
130                                 // set cursor depending on whether we're floating over the start of this way, 
131                                 // another random node, a possible junction...
132                                 if (entity is Node && focus is Way && Way(focus).endsWith(Node(entity))) {
133                                         if (focus==firstSelected) { controller.setCursor("pen_so"); }
134                                                              else { controller.setCursor("pen_o"); }
135                                 } else if (entity is Node) {
136                                         controller.setCursor("pen_x");
137                                 } else {
138                                         controller.setCursor("pen_plus");
139                                 }
140                         } else if ( event.type == MouseEvent.MOUSE_OUT && !isBackground ) {
141                                 if (focus is Way && entity!=firstSelected) {
142                                         hoverEntity=null;
143                                         layer.setHighlightOnNodes(focus as Way, { hoverway: false });
144                                         // ** We could do with an optional way of calling WayUI.redraw to only do the nodes, which would be a
145                                         // useful optimisation.
146                                 }
147                                 controller.setCursor("pen");
148                         }
149
150                         return this;
151                 }
152                 
153                 protected function resetElastic(node:Node):void {
154                         elastic.start = new Point(node.lon, node.latp);
155                         elastic.end   = new Point(controller.map.coord2lon(controller.map.mouseX),
156                                                   controller.map.coord2latp(controller.map.mouseY));
157                 }
158
159         /* Fix up the elastic after a WayNode event - e.g. triggered by undo */
160         private function fixElastic(event:Event):void {
161             if (firstSelected == null) return;
162             var node:Node;
163             if (editEnd) {
164               node = Way(firstSelected).getLastNode();
165             } else {
166               node = Way(firstSelected).getNode(0);
167             }
168             if (node) { //maybe selectedWay doesn't have any nodes left
169               elastic.start = new Point(node.lon, node.latp);
170             }
171         }
172
173                 override public function processKeyboardEvent(event:KeyboardEvent):ControllerState {
174                         switch (event.keyCode) {
175                                 case Keyboard.ENTER:                                    return keyExitDrawing();
176                                 case Keyboard.ESCAPE:                                   return keyExitDrawing();
177                                 case Keyboard.DELETE:           
178                                 case Keyboard.BACKSPACE:        
179                                 case 189: /* minus */       return backspaceNode(MainUndoStack.getGlobalStack().addAction);
180                                 case 79: /* O */                        return replaceNode();
181                 case 82:  /* R */           { if (! event.shiftKey) repeatTags(firstSelected); 
182                                               else                  repeatRelations(firstSelected);
183                                               return this; }
184                                 case 70: /* F */            followWay(); return this;
185                         }
186                         var cs:ControllerState = sharedKeyboardEvents(event);
187                         return cs ? cs : this;
188                         
189                 }
190
191                 public function replaceNode():ControllerState {
192                         var way:Way=Way(firstSelected);
193                         var oldNode:Node=editEnd ? way.getLastNode() : way.getNode(0);
194                         var newNode:Node=oldNode.replaceWithNew(layer.connection,
195                                                                 controller.map.coord2lat(layer.mouseY), 
196                                                                 controller.map.coord2lon(layer.mouseX), {},
197                                                                 MainUndoStack.getGlobalStack().addAction);
198                         var d:DragWayNode=new DragWayNode(way, way.indexOfNode(newNode), new MouseEvent(MouseEvent.CLICK, true, false, layer.mouseX, layer.mouseY), true);
199                         d.forceDragStart();
200                         return d;
201                 }
202                 
203                 protected function keyExitDrawing():ControllerState {
204                         var cs:ControllerState=stopDrawing();
205                         if (selectedWay.length==1) { 
206                                 return new NoSelection();
207                         }
208                         return cs;
209                 }
210                 
211                 protected function stopDrawing():ControllerState {
212                         if ( hoverEntity ) {
213                                 layer.setHighlightOnNodes(hoverEntity as Way, { hoverway: false });
214                                 hoverEntity = null;
215                         }
216
217                         if ( leaveNodeSelected ) {
218                             return new SelectedWayNode(firstSelected as Way, editEnd ? Way(firstSelected).length-1 : 0);
219                         } else {
220                             return new SelectedWay(firstSelected as Way);
221                         }
222                 }
223
224                 public function createAndAddNode(event:MouseEvent, performAction:Function):Node {
225                     var undo:CompositeUndoableAction = new CompositeUndoableAction("Add node");
226                     
227                         var lat:Number = controller.map.coord2lat(event.localY);
228                         var lon:Number = controller.map.coord2lon(event.localX);
229                         var node:Node = firstSelected.connection.createNode({}, lat, lon, undo.push);
230                         appendNode(node, undo.push);
231                         
232                         performAction(undo);
233                         return node;
234                 }
235                 
236                 protected function appendNode(node:Node, performAction:Function):void {
237                         if ( editEnd )
238                                 Way(firstSelected).appendNode(node, performAction);
239                         else
240                                 Way(firstSelected).prependNode(node, performAction);
241                 }
242                 
243                 protected function backspaceNode(performAction:Function):ControllerState {
244                         if (selectedWay.length==1) return keyExitDrawing();
245
246                         var node:Node;
247                         var undo:CompositeUndoableAction = new CompositeUndoableAction("Remove node");
248                         var newDraw:int;
249             var state:ControllerState;
250
251                         if (editEnd) {
252                                 node=Way(firstSelected).getLastNode();
253                                 Way(firstSelected).removeNodeByIndex(Way(firstSelected).length-1, undo.push);
254                                 newDraw=Way(firstSelected).length-2;
255                         } else {
256                                 node=Way(firstSelected).getNode(0);
257                                 Way(firstSelected).removeNodeByIndex(0, undo.push);
258                                 newDraw=0;
259                         }
260                         // Only actually delete the node if it has no other tags, and is not part of other ways (or part of this way twice)
261                         if (node.numParentWays==1 && Way(firstSelected).hasOnceOnly(node) && !node.hasInterestingTags()) {
262                                 layer.setPurgable([node], true);
263                                 node.connection.unregisterPOI(node);
264                                 node.remove(undo.push);
265                         }
266
267                         if (newDraw>=0 && newDraw<=Way(firstSelected).length-2) {
268                                 var mouse:Point = new Point(Way(firstSelected).getNode(newDraw).lon, Way(firstSelected).getNode(newDraw).latp);
269                                 elastic.start = mouse;
270                                 state = this;
271                         } else {
272                 Way(firstSelected).remove(undo.push);
273                 state = new NoSelection();
274                         }
275
276             performAction(undo);
277
278             if(!node.isDeleted()) { // i.e. was junction with another way (or is now POI)
279               layer.setHighlight(node, {selectedway: false});
280             }
281             return state;
282                 }
283                 
284                 /** Extends the current way by "following" an existing way, after the user has already selected two nodes in a row. 
285                         If drawing way has at least two nodes, and both belong to another way, and those ways are the same,
286                         then find the next node, add that node, update screen and scroll the new node into shot if necessary.
287                         TODO: add a bit of feedback (FloatingAlert?) when following can't be carried out for some reason. */
288                 protected function followWay():void {
289                         var curnode:Node;
290                         var prevnode:Node;
291                         if (Way(firstSelected).length < 2) return;
292
293                         if (editEnd) {
294                                 curnode = Way(firstSelected).getLastNode();
295                                 prevnode = Way(firstSelected).getNode(Way(firstSelected).length-2);
296                         } else {
297                                 curnode = Way(firstSelected).getNode(0);
298                                 prevnode = Way(firstSelected).getNode(1);
299                         }
300                         if (curnode.numParentWays <2 || prevnode.numParentWays <2) return;
301
302                         var followedWay:Way;
303                         for each (var way:Way in curnode.parentWays) {
304                                 if (way!=firstSelected && prevnode.hasParent(way))
305                                         followedWay = way;              // FIXME: could be smarter when there's more than one candidate
306                         }
307                         if (!followedWay) return;
308
309                         var nextNode:Node;
310                         if (followedWay.getNextNode(prevnode) == curnode) {
311                                 nextNode = followedWay.getNextNode(curnode);
312                         } else if (followedWay.getNextNode(curnode) == prevnode){
313                                 nextNode = followedWay.getPrevNode(curnode);
314                         } else if (followedWay.indexOfNode(curnode) > followedWay.indexOfNode(prevnode)) {
315                                 // The two nodes selected aren't actually consecutive. Make a half-hearted
316                                 // guess at which way to follow. Will be "incorrect" if the join in the loop
317                                 // is between the two points. 
318                                 nextNode = followedWay.getNextNode(curnode);
319                         } else {
320                                 nextNode = followedWay.getPrevNode(curnode);
321                         }
322                         if (!nextNode) return;
323                         if (nextNode.hasParent(firstSelected) && !(firstSelected as Way).hasOnceOnly(curnode)) return;
324
325                         appendNode(nextNode as Node, MainUndoStack.getGlobalStack().addAction);
326                         resetElastic(nextNode as Node);
327                         lastClick=nextNode;
328                         layer.setHighlight(nextNode, { selectedway: true });
329
330                         // recentre the map if the new lat/lon is offscreen
331                         controller.map.scrollIfNeeded(nextNode.lat,nextNode.lon);
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("pen");
344                 }
345                 override public function exitState(newState:ControllerState):void {
346                         if (selectedWay.length==1 && !MainUndoStack.getGlobalStack().undoIfAction(BeginWayAction)) { 
347                                 selectedWay.remove(MainUndoStack.getGlobalStack().addAction);
348                         }
349
350             Way(firstSelected).removeEventListener(Connection.WAY_NODE_REMOVED, fixElastic);
351             Way(firstSelected).removeEventListener(Connection.WAY_NODE_ADDED, fixElastic);
352
353                         super.exitState(newState);
354                         controller.setCursor();
355                         elastic.removeSprites();
356                         elastic = null;
357                 }
358                 override public function toString():String {
359                         return "DrawWay";
360                 }
361         }
362 }