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