Merge branch 'master' of github.com:systemed/potlatch2
[potlatch2.git] / net / systemeD / potlatch2 / controller / SelectedWayNode.as
1 package net.systemeD.potlatch2.controller {
2         import flash.events.*;
3         import flash.geom.Point;
4         import flash.ui.Keyboard;
5         
6         import net.systemeD.halcyon.AttentionEvent;
7         import net.systemeD.halcyon.WayUI;
8         import net.systemeD.halcyon.connection.*;
9         import net.systemeD.halcyon.connection.actions.*;
10         import net.systemeD.potlatch2.tools.Quadrilateralise;
11
12     public class SelectedWayNode extends ControllerState {
13                 private var parentWay:Way;
14                 private var initIndex:int;
15                 private var selectedIndex:int;
16                 private var shiftClickEvent:MouseEvent;
17         
18         public function SelectedWayNode(way:Way,index:int) {
19             parentWay = way;
20                         initIndex = index;
21         }
22
23                 override public function handleShiftClickOnEntry(event:MouseEvent):void {
24                         shiftClickEvent=event;
25                 }
26
27         protected function selectNode(way:Way,index:int):void {
28                         var node:Node=way.getNode(index);
29             if ( way == parentWay && node == firstSelected )
30                 return;
31
32             clearSelection(this);
33             layer.setHighlight(way, { hover: false });
34             layer.setHighlight(node, { selected: true });
35             layer.setHighlightOnNodes(way, { selectedway: true });
36             selection = [node]; parentWay = way;
37             controller.updateSelectionUI();
38                         selectedIndex = index; initIndex = index;
39         }
40                 
41         protected function clearSelection(newState:ControllerState):void {
42             if ( selectCount ) {
43                 layer.setHighlight(parentWay, { selected: false });
44                                 layer.setHighlight(firstSelected, { selected: false });
45                                 layer.setHighlightOnNodes(parentWay, { selectedway: false });
46                                 selection = [];
47                 if (!newState.isSelectionState()) { controller.updateSelectionUI(); }
48             }
49         }
50         
51         override public function processMouseEvent(event:MouseEvent, entity:Entity):ControllerState {
52                         if (event.type==MouseEvent.MOUSE_MOVE || event.type==MouseEvent.ROLL_OVER || event.type==MouseEvent.MOUSE_OUT) { return this; }
53             var focus:Entity = getTopLevelFocusEntity(entity);
54
55             if ( event.type == MouseEvent.MOUSE_UP && entity is Node && event.shiftKey ) {
56                                 // start new way
57                 var way:Way = entity.connection.createWay({}, [entity],
58                     MainUndoStack.getGlobalStack().addAction);
59                 return new DrawWay(way, true, false);
60                         } else if ( event.type == MouseEvent.MOUSE_UP && entity is Node && focus == parentWay ) {
61                                 // select node within way
62                                 return selectOrEdit(parentWay, getNodeIndex(parentWay,Node(entity)));
63             } else if ( event.type == MouseEvent.MOUSE_DOWN && entity is Way && focus==parentWay && event.shiftKey) {
64                                 // insert node within way (shift-click)
65                         var d:DragWayNode=new DragWayNode(parentWay, -1, event, true);
66                                 d.forceDragStart();
67                                 return d;
68                         } else if ( event.type == MouseEvent.MOUSE_UP && !entity && event.shiftKey ) {
69                                 // shift-clicked nearby to insert node
70                                 var lat:Number = controller.map.coord2lat(event.localY);
71                                 var lon:Number = controller.map.coord2lon(event.localX);
72                                 var undo:CompositeUndoableAction = new CompositeUndoableAction("Insert node");
73                                 parentWay.insertNodeOrMoveExisting(lat, lon, undo.push);
74                                 MainUndoStack.getGlobalStack().addAction(undo);
75                                 return new SelectedWay(parentWay);
76                         }
77                         var cs:ControllerState = sharedMouseEvents(event, entity);
78                         return cs ? cs : this;
79         }
80
81                 override public function processKeyboardEvent(event:KeyboardEvent):ControllerState {
82                         switch (event.keyCode) {
83                                 case 189:                                       return removeNode();                                    // '-'
84                                 case 88:                                        return splitWay();                                              // 'X'
85                                 case 78:                                        return otherEnd();                                              // 'N'
86                                 case 79:                                        return replaceNode();                                   // 'O'
87                 case 81:  /* Q */           Quadrilateralise.quadrilateralise(parentWay, MainUndoStack.getGlobalStack().addAction); return this;
88                 case 82:  /* R */           { if (! event.shiftKey) repeatTags(firstSelected); 
89                                               else                  repeatRelations(firstSelected);
90                                               return this; }
91                                 case 87:                                        return new SelectedWay(parentWay);              // 'W'
92                                 case 191:                                       return cycleWays();                                             // '/'
93                 case 74:                    if (event.shiftKey) { return unjoin() }; return join();// 'J'
94                                 case Keyboard.BACKSPACE:        return deleteNode();
95                                 case Keyboard.DELETE:           return deleteNode();
96                                 case 188: /* , */           return stepNode(event.shiftKey ? -10 : -1);
97                                 case 190: /* . */           return stepNode(event.shiftKey ? +10 : +1);
98                         }
99                         var cs:ControllerState = sharedKeyboardEvents(event);
100                         return cs ? cs : this;
101                 }
102
103                 override public function get selectedWay():Way {
104                         return parentWay;
105                 }
106
107         public function get selectedNode():Node {
108             return parentWay.getNode(selectedIndex);
109         }
110         
111                 private function cycleWays():ControllerState {
112                         var wayList:Array=firstSelected.parentWays;
113                         if (wayList.length==1) { return this; }
114                         wayList.splice(wayList.indexOf(parentWay),1);
115             // find index of this node in the newly selected way, to maintain state for keyboard navigation
116             var newindex:int = Way(wayList[0]).indexOfNode(parentWay.getNode(initIndex));
117                         return new SelectedWay(wayList[0], layer,
118                                                new Point(controller.map.lon2coord(Node(firstSelected).lon),
119                                                          controller.map.latp2coord(Node(firstSelected).latp)),
120                                                wayList.concat(parentWay),
121                                                newindex);
122                 }
123
124                 override public function enterState():void {
125                         if (shiftClickEvent) {
126                                 // previously shift-clicked nearby to insert node, passed through by ZoomArea
127                                 var lat:Number = controller.map.coord2lat(shiftClickEvent.localY);
128                                 var lon:Number = controller.map.coord2lon(shiftClickEvent.localX);
129                                 var undo:CompositeUndoableAction = new CompositeUndoableAction("Insert node");
130                                 parentWay.insertNodeOrMoveExisting(lat, lon, undo.push);
131                                 MainUndoStack.getGlobalStack().addAction(undo);
132                                 shiftClickEvent = null;
133                         }
134             selectNode(parentWay,initIndex);
135                         layer.setPurgable(selection,false);
136         }
137                 override public function exitState(newState:ControllerState):void {
138             if (firstSelected.hasTags()) {
139               controller.clipboards['node']=firstSelected.getTagsCopy();
140             }
141             copyRelations(firstSelected);
142                         layer.setPurgable(selection,true);
143             clearSelection(newState);
144         }
145
146         override public function toString():String {
147             return "SelectedWayNode";
148         }
149
150                 public static function selectOrEdit(selectedWay:Way, index:int):ControllerState {
151                         var isFirst:Boolean = false;
152                         var isLast:Boolean = false;
153                         var node:Node = selectedWay.getNode(index);
154                         isFirst = selectedWay.getNode(0) == node;
155                         isLast = selectedWay.getLastNode() == node;
156                         if ( isFirst == isLast )    // both == looped, none == central node 
157                             return new SelectedWayNode(selectedWay, index);
158                         else
159                             return new DrawWay(selectedWay, isLast, true);
160         }
161
162                 /** Replace the selected node with a new one created at the mouse position. 
163                         The undo for this is two actions: first, replacement of the old node at the original mouse position; then, moving to the new position.
164                         It's debatable whether this should be one or two but we can leave it as a FIXME for now.  */
165                 public function replaceNode():ControllerState {
166                         // replace old node
167                         var oldNode:Node=firstSelected as Node;
168                         var newNode:Node=oldNode.replaceWithNew(layer.connection,
169                                                                 controller.map.coord2lat(layer.mouseY), 
170                                                                 controller.map.coord2lon(layer.mouseX), {},
171                                                                 MainUndoStack.getGlobalStack().addAction);
172
173                         // start dragging
174                         // we fake a MouseEvent because DragWayNode expects the x/y co-ords to be passed that way
175                         var d:DragWayNode=new DragWayNode(parentWay, parentWay.indexOfNode(newNode), new MouseEvent(MouseEvent.CLICK, true, false, layer.mouseX, layer.mouseY), true);
176                         d.forceDragStart();
177                         return d;
178                 }
179
180                 /** Splits a way into two separate ways, at the currently selected node. Handles simple loops and P-shapes. Untested for anything funkier. */
181                 public function splitWay():ControllerState {
182                         var n:Node=firstSelected as Node;
183                         var ni:uint = parentWay.indexOfNode(n);
184                         // abort if start or end
185                         if (parentWay.isPShape() && !parentWay.hasOnceOnly(n)) {
186                                 // If P-shaped, we want to split at the midway point on the stem, not at the end of the loop
187                                 ni = parentWay.getPJunctionNodeIndex();
188                                 
189                         } else {
190                             if (parentWay.getNode(0)    == n) { return this; }
191                             if (parentWay.getLastNode() == n) { return this; }
192                         }
193
194                         layer.setHighlightOnNodes(parentWay, { selectedway: false } );
195                         layer.setPurgable([parentWay],true);
196             MainUndoStack.getGlobalStack().addAction(new SplitWayAction(parentWay, ni));
197
198                         return new SelectedWay(parentWay);
199                 }
200                 
201                 public function removeNode():ControllerState {
202                         if (firstSelected.numParentWays==1 && parentWay.hasOnceOnly(firstSelected as Node) && !(firstSelected as Node).hasInterestingTags()) {
203                                 return deleteNode();
204                         }
205                         parentWay.removeNodeByIndex(selectedIndex, MainUndoStack.getGlobalStack().addAction);
206                         return new SelectedWay(parentWay);
207                 }
208                 
209                 public function deleteNode():ControllerState {
210                         layer.setPurgable(selection,true);
211                         firstSelected.remove(MainUndoStack.getGlobalStack().addAction);
212                         return new SelectedWay(parentWay);
213                 }
214
215         public function unjoin():ControllerState {
216             Node(firstSelected).unjoin(parentWay, MainUndoStack.getGlobalStack().addAction);
217             return this;
218         }
219
220         /** Attempt to either merge the currently selected node with another very nearby node, or failing that,
221         *   attach it mid-way along a very nearby way. */
222                 // FIXME: why are we only merging one node at once? after all, shift-click to insert a node adds into all ways
223         public function join():ControllerState {
224                         var p:Point = new Point(controller.map.lon2coord(Node(firstSelected).lon),
225                                                 controller.map.latp2coord(Node(firstSelected).latp));
226             var q:Point = map.localToGlobal(p);
227
228             // First, look for POI nodes in 20x20 pixel box around the current node
229                         // FIXME: why aren't we using a hitTest for this?
230             var hitnodes:Array = layer.connection.getObjectsByBbox(
231                 map.coord2lon(p.x-10),
232                 map.coord2lon(p.x+10),
233                 map.coord2lat(p.y-10),
234                 map.coord2lat(p.y+10)).poisInside;
235             
236             for each (var n: Node in hitnodes) {
237                 if (!n.hasParent(selectedWay)) { 
238                    return doMergeNodes(n);
239                 }
240             }
241             
242                         var ways:Array=layer.findWaysAtPoint(q.x, q.y, selectedWay);
243                         for each (var w:Way in ways) {
244                 // hit a way, now let's see if we hit a specific node
245                 for (var i:uint = 0; i < w.length; i++) {
246                                         n = w.getNode(i);
247                                         var x:Number = map.lon2coord(n.lon);
248                                         var y:Number = map.latp2coord(n.latp);
249                                         if (n != selectedNode && Math.abs(x-p.x) + Math.abs(y-p.y) < 10) {
250                                                 return doMergeNodes(n);
251                                         }
252                                 }
253             }
254
255             // No nodes hit, so join our node onto any overlapping ways.
256             Node(firstSelected).join(ways,MainUndoStack.getGlobalStack().addAction);
257             return this;
258         }
259         
260         private function doMergeNodes(n:Node): ControllerState {
261                 var nways:Array = n.parentWays.concat(Node(firstSelected).parentWays);
262                 var mna:MergeNodesAction = n.mergeWith(Node(firstSelected), MainUndoStack.getGlobalStack().addAction);
263             /* Duplicated consecutive nodes happen if the two merged nodes are consecutive nodes of a (different) way */
264             for each (var w:Way in nways) {
265                // If there's a node to remove, jam that action into the existing MergeNodesAction. 
266                w.removeRepeatedNodes(function (a:UndoableAction):void { a.doAction(); mna.push(a); } );
267             }
268                
269             // only merge one node at a time - too confusing otherwise?
270             var msg:String = "Nodes merged"
271             if (MergeNodesAction.lastTagsMerged) msg += ": check conflicting tags";
272             controller.dispatchEvent(new AttentionEvent(AttentionEvent.ALERT, null, msg));
273                         if (n.isDeleted()) n=Node(firstSelected);
274             return new SelectedWayNode(n.parentWays[0], Way(n.parentWays[0]).indexOfNode(n));
275         }
276         
277                 /** Move the selection one node further up or down this way, looping if necessary. */
278                 public function stepNode(delta:int):ControllerState {
279                         var ni:int;
280                         if (Math.abs(delta)==1) {
281                                 ni = (selectedIndex + delta + parentWay.length) % parentWay.length;
282                         } else if (delta<0) {
283                                 ni = Math.max(selectedIndex+delta, 0);
284                         } else {
285                                 ni = Math.min(selectedIndex+delta, parentWay.length-1);
286                         }
287                         controller.map.scrollIfNeeded(parentWay.getNode(ni).lat,parentWay.getNode(ni).lon);
288                         return new SelectedWayNode(parentWay, ni);
289                 }
290
291                 /** Jump to the other end of the way **/
292                 public function otherEnd():ControllerState {
293                         var n:Node = parentWay.getFirstNode().within(map.edge_l, map.edge_r, map.edge_t, map.edge_b) ? parentWay.getLastNode() : parentWay.getFirstNode();
294                 controller.map.scrollIfNeeded(n.lat,n.lon);
295                         return new SelectedWay(parentWay);
296                 }
297
298     }
299     
300     
301 }
302