ADD ability to merge one node with another (including POI nodes)
[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.Globals;
8         import net.systemeD.halcyon.WayUI;
9         import net.systemeD.halcyon.connection.*;
10         import net.systemeD.halcyon.connection.actions.*;
11         import net.systemeD.potlatch2.tools.Quadrilateralise;
12
13     public class SelectedWayNode extends ControllerState {
14                 private var parentWay:Way;
15                 private var initIndex:int;
16                 private var selectedIndex:int;
17         
18         public function SelectedWayNode(way:Way,index:int) {
19             parentWay = way;
20                         initIndex = index;
21         }
22  
23         protected function selectNode(way:Way,index:int):void {
24                         var node:Node=way.getNode(index);
25             if ( way == parentWay && node == firstSelected )
26                 return;
27
28             clearSelection(this);
29             controller.map.setHighlight(way, { hover: false });
30             controller.map.setHighlight(node, { selected: true });
31             controller.map.setHighlightOnNodes(way, { selectedway: true });
32             selection = [node]; parentWay = way;
33             controller.updateSelectionUI();
34                         selectedIndex = index; initIndex = index;
35         }
36                 
37         protected function clearSelection(newState:ControllerState):void {
38             if ( selectCount ) {
39                 controller.map.setHighlight(parentWay, { selected: false });
40                                 controller.map.setHighlight(firstSelected, { selected: false });
41                                 controller.map.setHighlightOnNodes(parentWay, { selectedway: false });
42                                 selection = [];
43                 if (!newState.isSelectionState()) { controller.updateSelectionUI(); }
44             }
45         }
46         
47         override public function processMouseEvent(event:MouseEvent, entity:Entity):ControllerState {
48                         if (event.type==MouseEvent.MOUSE_MOVE || event.type==MouseEvent.ROLL_OVER || event.type==MouseEvent.MOUSE_OUT) { return this; }
49             var focus:Entity = getTopLevelFocusEntity(entity);
50
51             if ( event.type == MouseEvent.MOUSE_UP && entity is Node && event.shiftKey ) {
52                                 // start new way
53                 var way:Way = controller.connection.createWay({}, [entity],
54                     MainUndoStack.getGlobalStack().addAction);
55                 return new DrawWay(way, true, false);
56                         } else if ( event.type == MouseEvent.MOUSE_UP && entity is Node && focus == parentWay ) {
57                                 // select node within way
58                                 return selectOrEdit(parentWay, getNodeIndex(parentWay,Node(entity)));
59             } else if ( event.type == MouseEvent.MOUSE_DOWN && entity is Way && focus==parentWay && event.shiftKey) {
60                                 // insert node within way (shift-click)
61                         var d:DragWayNode=new DragWayNode(parentWay, -1, event, true);
62                                 d.forceDragStart();
63                                 return d;
64                         }
65                         var cs:ControllerState = sharedMouseEvents(event, entity);
66                         return cs ? cs : this;
67         }
68
69                 override public function processKeyboardEvent(event:KeyboardEvent):ControllerState {
70                         switch (event.keyCode) {
71                                 case 189:                                       return removeNode();                                    // '-'
72                                 case 88:                                        return splitWay();                                              // 'X'
73                 case 81:  /* Q */           Quadrilateralise.quadrilateralise(parentWay, MainUndoStack.getGlobalStack().addAction); return this;
74                                 case 82:                                        repeatTags(firstSelected); return this; // 'R'
75                                 case 87:                                        return new SelectedWay(parentWay);              // 'W'
76                                 case 191:                                       return cycleWays();                                             // '/'
77                 case 74:                    if (event.shiftKey) { return unjoin() }; return join();// 'J'
78                                 case Keyboard.BACKSPACE:        return deleteNode();
79                                 case Keyboard.DELETE:           return deleteNode();
80                         }
81                         var cs:ControllerState = sharedKeyboardEvents(event);
82                         return cs ? cs : this;
83                 }
84
85                 override public function get selectedWay():Way {
86                         return parentWay;
87                 }
88
89         public function get selectedNode():Node {
90             return parentWay.getNode(selectedIndex);
91         }
92
93
94                 private function cycleWays():ControllerState {
95                         var wayList:Array=firstSelected.parentWays;
96                         if (wayList.length==1) { return this; }
97                         wayList.splice(wayList.indexOf(parentWay),1);
98                         return new SelectedWay(wayList[0],
99                                                new Point(controller.map.lon2coord(Node(firstSelected).lon),
100                                                          controller.map.latp2coord(Node(firstSelected).latp)),
101                                                wayList.concat(parentWay));
102                 }
103
104                 override public function enterState():void {
105             selectNode(parentWay,initIndex);
106                         controller.map.setPurgable(selection,false);
107                         Globals.vars.root.addDebug("**** -> "+this);
108         }
109                 override public function exitState(newState:ControllerState):void {
110             if (firstSelected.hasTags()) {
111               controller.clipboards['node']=firstSelected.getTagsCopy();
112             }
113                         controller.map.setPurgable(selection,true);
114             clearSelection(newState);
115                         Globals.vars.root.addDebug("**** <- "+this);
116         }
117
118         override public function toString():String {
119             return "SelectedWayNode";
120         }
121
122                 public static function selectOrEdit(selectedWay:Way, index:int):ControllerState {
123                         var isFirst:Boolean = false;
124                         var isLast:Boolean = false;
125                         var node:Node = selectedWay.getNode(index);
126                         isFirst = selectedWay.getNode(0) == node;
127                         isLast = selectedWay.getLastNode() == node;
128                         if ( isFirst == isLast )    // both == looped, none == central node 
129                             return new SelectedWayNode(selectedWay, index);
130                         else
131                             return new DrawWay(selectedWay, isLast, true);
132         }
133
134                 /** Splits a way into two separate ways, at the currently selected node. Handles simple loops and P-shapes. Untested for anything funkier. */
135                 public function splitWay():ControllerState {
136                         var n:Node=firstSelected as Node;
137                         var ni:uint = parentWay.indexOfNode(n);
138                         // abort if start or end
139                         if (parentWay.isPShape() && !parentWay.hasOnceOnly(n)) {
140                                 // If P-shaped, we want to split at the midway point on the stem, not at the end of the loop
141                                 ni = parentWay.getPJunctionNodeIndex();
142                                 
143                         } else {
144                             if (parentWay.getNode(0)    == n) { return this; }
145                             if (parentWay.getLastNode() == n) { return this; }
146                         }
147
148                         controller.map.setHighlightOnNodes(parentWay, { selectedway: false } );
149                         controller.map.setPurgable([parentWay],true);
150             MainUndoStack.getGlobalStack().addAction(new SplitWayAction(parentWay, ni));
151
152                         return new SelectedWay(parentWay);
153                 }
154                 
155                 public function removeNode():ControllerState {
156                         if (firstSelected.numParentWays==1 && parentWay.hasOnceOnly(firstSelected as Node) && !(firstSelected as Node).hasInterestingTags()) {
157                                 return deleteNode();
158                         }
159                         parentWay.removeNodeByIndex(selectedIndex, MainUndoStack.getGlobalStack().addAction);
160                         return new SelectedWay(parentWay);
161                 }
162                 
163                 public function deleteNode():ControllerState {
164                         controller.map.setPurgable(selection,true);
165                         firstSelected.remove(MainUndoStack.getGlobalStack().addAction);
166                         return new SelectedWay(parentWay);
167                 }
168
169         public function unjoin():ControllerState {
170             Node(firstSelected).unjoin(parentWay, MainUndoStack.getGlobalStack().addAction);
171             return this;
172         }
173
174         /** Attempt to either merge the currently selected node with another very nearby node, or failing that,
175         *   attach it mid-way along a very nearby way. */
176         public function join():ControllerState {
177             var p:Point = new Point(controller.map.lon2coord(Node(firstSelected).lon),
178                                              controller.map.latp2coord(Node(firstSelected).latp));
179             var q:Point = map.localToGlobal(p);
180
181             // First, look for POI nodes in 20x20 pixel box around the current node
182             var hitnodes:Array = map.connection.getObjectsByBbox(
183                 map.coord2lon(p.x-10),
184                 map.coord2lon(p.x+10),
185                 map.coord2lat(p.y-10),
186                 map.coord2lat(p.y+10)).poisInside;
187             
188             for each (var n: Node in hitnodes) {
189                 if (!n.hasParent(selectedWay)) { 
190                    return doMergeNodes(n);
191                 }
192             }
193             
194             var ways:Array=[]; var w:Way;
195             for each (var wayui:WayUI in controller.map.paint.wayuis) {
196                 w=wayui.hitTest(q.x, q.y);
197                 if (w && w!=selectedWay) { 
198                 // hit a way, now let's see if we hit a specific node
199                     for (var i:uint = 0; i < w.length; i++) {
200                         n = w.getNode(i);
201                         var x:Number = map.lon2coord(n.lon);
202                         var y:Number = map.latp2coord(n.latp);
203                         if (n != selectedNode && Math.abs(x-p.x) + Math.abs(y-p.y) < 10) {
204                             return doMergeNodes(n);
205                         }    
206                     }
207                     ways.push(w); 
208                 }
209             }
210
211             // No nodes hit, so join our node onto any overlapping ways.
212             Node(firstSelected).join(ways,MainUndoStack.getGlobalStack().addAction);
213             return this;
214         }
215         
216         private function doMergeNodes(n:Node): ControllerState {
217                 n.mergeWith(Node(firstSelected), MainUndoStack.getGlobalStack().addAction);
218             // only merge one node at a time - too confusing otherwise?
219             var msg:String = "Nodes merged."
220             if (MergeNodesAction.lastProblemTags) {
221                 msg += " *Warning* The following tags conflicted and need attention: " + MergeNodesAction.lastProblemTags;
222             }
223             map.connection.dispatchEvent(new AttentionEvent(AttentionEvent.ALERT, null, msg));
224             return new SelectedWayNode(n.parentWays[0], Way(n.parentWays[0]).indexOfNode(n));
225         }
226     }
227 }
228