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