1 package net.systemeD.potlatch2.controller {
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.Globals;
11 import net.systemeD.halcyon.MapPaint;
13 public class DrawWay extends SelectedWay {
14 private var elastic:Elastic;
15 private var editEnd:Boolean; // if true, we're drawing from node[n-1], else "backwards" from node[0]
16 private var leaveNodeSelected:Boolean;
17 private var lastClick:Entity=null;
18 private var lastClickTime:Date;
19 private var hoverEntity:Entity; // keep track of the currently rolled-over object, because
20 // Flash can fire a mouseDown from the map even if you
21 // haven't rolled out of the way
23 public function DrawWay(way:Way, editEnd:Boolean, leaveNodeSelected:Boolean) {
25 this.editEnd = editEnd;
26 this.leaveNodeSelected = leaveNodeSelected;
27 if (way.length==1 && way.getNode(0).parentWays.length==1) {
28 // drawing new way, so keep track of click in case creating a POI
29 lastClick=way.getNode(0);
30 lastClickTime=new Date();
34 override public function processMouseEvent(event:MouseEvent, entity:Entity):ControllerState {
37 var paint:MapPaint = getMapPaint(DisplayObject(event.target));
38 var isBackground:Boolean = paint && paint.isBackground;
40 if (entity == null && hoverEntity) { entity=hoverEntity; }
41 var focus:Entity = getTopLevelFocusEntity(entity);
43 if ( event.type == MouseEvent.MOUSE_UP ) {
44 controller.map.mouseUpHandler(); // in case you're still in the drag-tolerance zone, and mouse up over something.
45 if ( entity == null || isBackground ) { // didn't hit anything: extend the way by one node.
46 node = createAndAddNode(event, MainUndoStack.getGlobalStack().addAction);
47 controller.map.setHighlight(node, { selectedway: true });
48 controller.map.setPurgable([node], false);
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.
56 MainUndoStack.getGlobalStack().undo(); // undo the BeginWayAction that (presumably?) just happened
58 var newPoiAction:CreatePOIAction = new CreatePOIAction(
60 controller.map.coord2lat(event.localY),
61 controller.map.coord2lon(event.localX));
62 MainUndoStack.getGlobalStack().addAction(newPoiAction);
63 return new SelectedPOINode(newPoiAction.getNode());
65 // double-click at end of way
68 } else if (entity==lastClick) {
69 // clicked slowly on the end node - do nothing
72 // hit a node, add it to this way and carry on
73 appendNode(entity as Node, MainUndoStack.getGlobalStack().addAction);
75 controller.map.setHighlightOnNodes(focus as Way, { hoverway: false });
77 controller.map.setHighlight(entity, { selectedway: true });
78 resetElastic(entity as Node);
80 if (Way(firstSelected).getNode(0)==Way(firstSelected).getLastNode()) {
81 // the node just hit completes a loop, so stop drawing.
82 return new SelectedWay(firstSelected as Way);
85 } else if ( entity is Way ) {
86 if (entity==firstSelected) {
87 // add junction node - self-intersecting way
88 var lat:Number = controller.map.coord2lat(event.localY);
89 var lon:Number = controller.map.coord2lon(event.localX);
90 var undo:CompositeUndoableAction = new CompositeUndoableAction("Insert node");
91 node = controller.connection.createNode({}, lat, lon, undo.push);
92 Way(firstSelected).insertNodeAtClosestPosition(node, true, undo.push);
93 appendNode(node,undo.push);
94 MainUndoStack.getGlobalStack().addAction(undo);
96 // add junction node - another way
97 var jnct:CompositeUndoableAction = new CompositeUndoableAction("Junction Node");
98 node = createAndAddNode(event, jnct.push);
99 Way(entity).insertNodeAtClosestPosition(node, true, jnct.push);
100 MainUndoStack.getGlobalStack().addAction(jnct);
101 controller.map.setHighlight(node, { selectedway: true });
102 controller.map.setPurgable([node], false);
106 controller.map.setHighlightOnNodes(entity as Way, { hoverway: false });
107 controller.map.setHighlightOnNodes(firstSelected as Way, { selectedway: true });
109 lastClickTime=new Date();
110 } else if ( event.type == MouseEvent.MOUSE_MOVE && elastic ) {
111 // mouse is roaming around freely
113 controller.map.coord2lon(event.localX),
114 controller.map.coord2latp(event.localY));
116 } else if ( event.type == MouseEvent.ROLL_OVER && !isBackground ) {
117 // mouse has floated over something
118 if (focus is Way && focus!=firstSelected) {
119 // floating over another way, highlight its nodes
121 controller.map.setHighlightOnNodes(focus as Way, { hoverway: true });
123 // set cursor depending on whether we're floating over the start of this way,
124 // another random node, a possible junction...
125 if (entity is Node && focus is Way && Way(focus).endsWith(Node(entity))) {
126 if (focus==firstSelected) { controller.setCursor(controller.pen_so); }
127 else { controller.setCursor(controller.pen_o); }
128 } else if (entity is Node) {
129 controller.setCursor(controller.pen_x);
131 controller.setCursor(controller.pen_plus);
133 } else if ( event.type == MouseEvent.MOUSE_OUT && !isBackground ) {
134 if (focus is Way && entity!=firstSelected) {
136 controller.map.setHighlightOnNodes(focus as Way, { hoverway: false });
137 // ** We could do with an optional way of calling WayUI.redraw to only do the nodes, which would be a
138 // useful optimisation.
140 controller.setCursor(controller.pen);
146 protected function resetElastic(node:Node):void {
147 elastic.start = new Point(node.lon, node.latp);
148 elastic.end = new Point(controller.map.coord2lon(controller.map.mouseX),
149 controller.map.coord2latp(controller.map.mouseY));
152 /* Fix up the elastic after a WayNode event - e.g. triggered by undo */
153 private function fixElastic(event:Event):void {
154 if (firstSelected == null) return;
157 node = Way(firstSelected).getLastNode();
159 node = Way(firstSelected).getNode(0);
161 if (node) { //maybe selectedWay doesn't have any nodes left
162 elastic.start = new Point(node.lon, node.latp);
166 override public function processKeyboardEvent(event:KeyboardEvent):ControllerState {
167 switch (event.keyCode) {
168 case Keyboard.ENTER: return keyExitDrawing();
169 case Keyboard.ESCAPE: return keyExitDrawing();
170 case Keyboard.DELETE:
171 case Keyboard.BACKSPACE:
172 case 189: /* minus */ return backspaceNode(MainUndoStack.getGlobalStack().addAction);
173 case 82: /* R */ repeatTags(firstSelected); return this;
174 case 70: /* F */ followWay(); return this;
176 var cs:ControllerState = sharedKeyboardEvents(event);
177 return cs ? cs : this;
181 protected function keyExitDrawing():ControllerState {
182 var cs:ControllerState=stopDrawing();
183 if (selectedWay.length==1) {
184 if (MainUndoStack.getGlobalStack().undoIfAction(BeginWayAction)) {
185 return new NoSelection();
192 protected function stopDrawing():ControllerState {
194 controller.map.setHighlightOnNodes(hoverEntity as Way, { hoverway: false });
198 if ( leaveNodeSelected ) {
199 return new SelectedWayNode(firstSelected as Way, editEnd ? Way(firstSelected).length-1 : 0);
201 return new SelectedWay(firstSelected as Way);
205 public function createAndAddNode(event:MouseEvent, performAction:Function):Node {
206 var undo:CompositeUndoableAction = new CompositeUndoableAction("Add node");
208 var lat:Number = controller.map.coord2lat(event.localY);
209 var lon:Number = controller.map.coord2lon(event.localX);
210 var node:Node = controller.connection.createNode({}, lat, lon, undo.push);
211 appendNode(node, undo.push);
217 protected function appendNode(node:Node, performAction:Function):void {
219 Way(firstSelected).appendNode(node, performAction);
221 Way(firstSelected).insertNode(0, node, performAction);
224 protected function backspaceNode(performAction:Function):ControllerState {
225 if (selectedWay.length==1) return keyExitDrawing();
228 var undo:CompositeUndoableAction = new CompositeUndoableAction("Remove node");
230 var state:ControllerState;
233 node=Way(firstSelected).getLastNode();
234 Way(firstSelected).removeNodeByIndex(Way(firstSelected).length-1, undo.push);
235 newDraw=Way(firstSelected).length-2;
237 node=Way(firstSelected).getNode(0);
238 Way(firstSelected).removeNodeByIndex(0, undo.push);
241 // Only actually delete the node if it has no other tags, and is not part of other ways (or part of this way twice)
242 if (node.numParentWays==1 && Way(firstSelected).hasOnceOnly(node) && !node.hasInterestingTags()) {
243 controller.map.setPurgable([node], true);
244 controller.connection.unregisterPOI(node);
245 node.remove(undo.push);
248 if (newDraw>=0 && newDraw<=Way(firstSelected).length-2) {
249 var mouse:Point = new Point(Way(firstSelected).getNode(newDraw).lon, Way(firstSelected).getNode(newDraw).latp);
250 elastic.start = mouse;
253 Way(firstSelected).remove(undo.push);
254 state = new NoSelection();
259 if(!node.isDeleted()) { // i.e. was junction with another way (or is now POI)
260 controller.map.setHighlight(node, {selectedway: false});
265 /** Extends the current way by "following" an existing way, after the user has already selected two nodes in a row.
266 If drawing way has at least two nodes, and both belong to another way, and those ways are the same,
267 then find the next node, add that node, update screen and scroll the new node into shot if necessary.
268 TODO: add a bit of feedback (FloatingAlert?) when following can't be carried out for some reason. */
269 protected function followWay():void {
272 if (Way(firstSelected).length < 2) return;
275 curnode = Way(firstSelected).getLastNode();
276 prevnode = Way(firstSelected).getNode(Way(firstSelected).length-2);
278 curnode = Way(firstSelected).getNode(0);
279 prevnode = Way(firstSelected).getNode(1);
281 if (curnode.numParentWays <2 || prevnode.numParentWays <2) return;
284 for each (var way:Way in curnode.parentWays) {
285 if (way!=firstSelected && prevnode.hasParent(way))
286 followedWay = way; // FIXME: could be smarter when there's more than one candidate
288 if (!followedWay) return;
291 if (followedWay.getNextNode(prevnode) == curnode) {
292 nextNode = followedWay.getNextNode(curnode);
293 } else if (followedWay.getNextNode(curnode) == prevnode){
294 nextNode = followedWay.getPrevNode(curnode);
295 } else if (followedWay.indexOfNode(curnode) > followedWay.indexOfNode(prevnode)) {
296 // The two nodes selected aren't actually consecutive. Make a half-hearted
297 // guess at which way to follow. Will be "incorrect" if the join in the loop
298 // is between the two points.
299 nextNode = followedWay.getNextNode(curnode);
301 nextNode = followedWay.getPrevNode(curnode);
303 if (!nextNode) return;
304 if (nextNode.hasParent(firstSelected)) return;
306 appendNode(nextNode as Node, MainUndoStack.getGlobalStack().addAction);
307 resetElastic(nextNode as Node);
309 controller.map.setHighlight(nextNode, { selectedway: true });
311 // recentre the map if the new lat/lon is offscreen
312 if (nextNode.lat > controller.map.edge_t ||
313 nextNode.lat < controller.map.edge_b ||
314 nextNode.lon < controller.map.edge_l ||
315 nextNode.lon > controller.map.edge_r) {
316 controller.map.moveMapFromLatLon(nextNode.lat, nextNode.lon);
320 override public function enterState():void {
323 Way(firstSelected).addEventListener(Connection.WAY_NODE_REMOVED, fixElastic);
324 Way(firstSelected).addEventListener(Connection.WAY_NODE_ADDED, fixElastic);
326 var node:Node = Way(firstSelected).getNode(editEnd ? Way(firstSelected).length-1 : 0);
327 var start:Point = new Point(node.lon, node.latp);
328 elastic = new Elastic(controller.map, start, start);
329 controller.setCursor(controller.pen);
330 Globals.vars.root.addDebug("**** -> "+this);
332 override public function exitState(newState:ControllerState):void {
333 Way(firstSelected).removeEventListener(Connection.WAY_NODE_REMOVED, fixElastic);
334 Way(firstSelected).removeEventListener(Connection.WAY_NODE_ADDED, fixElastic);
336 super.exitState(newState);
337 controller.setCursor(null);
338 elastic.removeSprites();
340 Globals.vars.root.addDebug("**** <- "+this);
342 override public function toString():String {