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 controller.updateSelectionUIWithoutTagChange();
52 } else if ( entity is Node ) {
53 if (entity==lastClick && (new Date().getTime()-lastClickTime.getTime())<1000) {
54 if (Way(firstSelected).length==1 && Way(firstSelected).getNode(0).parentWays.length==1) {
55 // Actually the user double-clicked to make a new node, they didn't want to draw a way at all.
57 MainUndoStack.getGlobalStack().undo(); // undo the BeginWayAction that (presumably?) just happened
59 var newPoiAction:CreatePOIAction = new CreatePOIAction(
61 controller.map.coord2lat(event.localY),
62 controller.map.coord2lon(event.localX));
63 MainUndoStack.getGlobalStack().addAction(newPoiAction);
64 return new SelectedPOINode(newPoiAction.getNode());
66 // double-click at end of way
69 } else if (entity==lastClick) {
70 // clicked slowly on the end node - do nothing
73 // hit a node, add it to this way and carry on
74 appendNode(entity as Node, MainUndoStack.getGlobalStack().addAction);
76 controller.map.setHighlightOnNodes(focus as Way, { hoverway: false });
78 controller.map.setHighlight(entity, { selectedway: true });
79 resetElastic(entity as Node);
81 if (Way(firstSelected).getNode(0)==Way(firstSelected).getLastNode()) {
82 // the node just hit completes a loop, so stop drawing.
83 return new SelectedWay(firstSelected as Way);
86 } else if ( entity is Way ) {
87 if (entity==firstSelected) {
88 // add junction node - self-intersecting way
89 var lat:Number = controller.map.coord2lat(event.localY);
90 var lon:Number = controller.map.coord2lon(event.localX);
91 var undo:CompositeUndoableAction = new CompositeUndoableAction("Insert node");
92 node = controller.connection.createNode({}, lat, lon, undo.push);
93 Way(firstSelected).insertNodeAtClosestPosition(node, true, undo.push);
94 appendNode(node,undo.push);
95 MainUndoStack.getGlobalStack().addAction(undo);
97 // add junction node - another way
98 var jnct:CompositeUndoableAction = new CompositeUndoableAction("Junction Node");
99 node = createAndAddNode(event, jnct.push);
100 Way(entity).insertNodeAtClosestPosition(node, true, jnct.push);
101 MainUndoStack.getGlobalStack().addAction(jnct);
102 controller.map.setHighlight(node, { selectedway: true });
103 controller.map.setPurgable([node], false);
107 controller.map.setHighlightOnNodes(entity as Way, { hoverway: false });
108 controller.map.setHighlightOnNodes(firstSelected as Way, { selectedway: true });
110 lastClickTime=new Date();
111 } else if ( event.type == MouseEvent.MOUSE_MOVE && elastic ) {
112 // mouse is roaming around freely
114 controller.map.coord2lon(event.localX),
115 controller.map.coord2latp(event.localY));
117 } else if ( event.type == MouseEvent.ROLL_OVER && !isBackground ) {
118 // mouse has floated over something
119 if (focus is Way && focus!=firstSelected) {
120 // floating over another way, highlight its nodes
122 controller.map.setHighlightOnNodes(focus as Way, { hoverway: true });
124 // set cursor depending on whether we're floating over the start of this way,
125 // another random node, a possible junction...
126 if (entity is Node && focus is Way && Way(focus).endsWith(Node(entity))) {
127 if (focus==firstSelected) { controller.setCursor(controller.pen_so); }
128 else { controller.setCursor(controller.pen_o); }
129 } else if (entity is Node) {
130 controller.setCursor(controller.pen_x);
132 controller.setCursor(controller.pen_plus);
134 } else if ( event.type == MouseEvent.MOUSE_OUT && !isBackground ) {
135 if (focus is Way && entity!=firstSelected) {
137 controller.map.setHighlightOnNodes(focus as Way, { hoverway: false });
138 // ** We could do with an optional way of calling WayUI.redraw to only do the nodes, which would be a
139 // useful optimisation.
141 controller.setCursor(controller.pen);
147 protected function resetElastic(node:Node):void {
148 elastic.start = new Point(node.lon, node.latp);
149 elastic.end = new Point(controller.map.coord2lon(controller.map.mouseX),
150 controller.map.coord2latp(controller.map.mouseY));
153 /* Fix up the elastic after a WayNode event - e.g. triggered by undo */
154 private function fixElastic(event:Event):void {
155 if (firstSelected == null) return;
158 node = Way(firstSelected).getLastNode();
160 node = Way(firstSelected).getNode(0);
162 if (node) { //maybe selectedWay doesn't have any nodes left
163 elastic.start = new Point(node.lon, node.latp);
167 override public function processKeyboardEvent(event:KeyboardEvent):ControllerState {
168 switch (event.keyCode) {
169 case Keyboard.ENTER: return keyExitDrawing();
170 case Keyboard.ESCAPE: return keyExitDrawing();
171 case Keyboard.DELETE:
172 case Keyboard.BACKSPACE:
173 case 189: /* minus */ return backspaceNode(MainUndoStack.getGlobalStack().addAction);
174 case 82: /* R */ repeatTags(firstSelected); return this;
175 case 70: /* F */ followWay(); return this;
177 var cs:ControllerState = sharedKeyboardEvents(event);
178 return cs ? cs : this;
182 protected function keyExitDrawing():ControllerState {
183 var cs:ControllerState=stopDrawing();
184 if (selectedWay.length==1) {
185 if (MainUndoStack.getGlobalStack().undoIfAction(BeginWayAction)) {
186 return new NoSelection();
193 protected function stopDrawing():ControllerState {
195 controller.map.setHighlightOnNodes(hoverEntity as Way, { hoverway: false });
199 if ( leaveNodeSelected ) {
200 return new SelectedWayNode(firstSelected as Way, editEnd ? Way(firstSelected).length-1 : 0);
202 return new SelectedWay(firstSelected as Way);
206 public function createAndAddNode(event:MouseEvent, performAction:Function):Node {
207 var undo:CompositeUndoableAction = new CompositeUndoableAction("Add node");
209 var lat:Number = controller.map.coord2lat(event.localY);
210 var lon:Number = controller.map.coord2lon(event.localX);
211 var node:Node = controller.connection.createNode({}, lat, lon, undo.push);
212 appendNode(node, undo.push);
218 protected function appendNode(node:Node, performAction:Function):void {
220 Way(firstSelected).appendNode(node, performAction);
222 Way(firstSelected).insertNode(0, node, performAction);
225 protected function backspaceNode(performAction:Function):ControllerState {
226 if (selectedWay.length==1) return keyExitDrawing();
229 var undo:CompositeUndoableAction = new CompositeUndoableAction("Remove node");
231 var state:ControllerState;
234 node=Way(firstSelected).getLastNode();
235 Way(firstSelected).removeNodeByIndex(Way(firstSelected).length-1, undo.push);
236 newDraw=Way(firstSelected).length-2;
238 node=Way(firstSelected).getNode(0);
239 Way(firstSelected).removeNodeByIndex(0, undo.push);
242 // Only actually delete the node if it has no other tags, and is not part of other ways (or part of this way twice)
243 if (node.numParentWays==1 && Way(firstSelected).hasOnceOnly(node) && !node.hasInterestingTags()) {
244 controller.map.setPurgable([node], true);
245 controller.connection.unregisterPOI(node);
246 node.remove(undo.push);
249 if (newDraw>=0 && newDraw<=Way(firstSelected).length-2) {
250 var mouse:Point = new Point(Way(firstSelected).getNode(newDraw).lon, Way(firstSelected).getNode(newDraw).latp);
251 elastic.start = mouse;
254 Way(firstSelected).remove(undo.push);
255 state = new NoSelection();
260 if(!node.isDeleted()) { // i.e. was junction with another way (or is now POI)
261 controller.map.setHighlight(node, {selectedway: false});
266 /** Extends the current way by "following" an existing way, after the user has already selected two nodes in a row.
267 If drawing way has at least two nodes, and both belong to another way, and those ways are the same,
268 then find the next node, add that node, update screen and scroll the new node into shot if necessary.
269 TODO: add a bit of feedback (FloatingAlert?) when following can't be carried out for some reason. */
270 protected function followWay():void {
273 if (Way(firstSelected).length < 2) return;
276 curnode = Way(firstSelected).getLastNode();
277 prevnode = Way(firstSelected).getNode(Way(firstSelected).length-2);
279 curnode = Way(firstSelected).getNode(0);
280 prevnode = Way(firstSelected).getNode(1);
282 if (curnode.numParentWays <2 || prevnode.numParentWays <2) return;
285 for each (var way:Way in curnode.parentWays) {
286 if (way!=firstSelected && prevnode.hasParent(way))
287 followedWay = way; // FIXME: could be smarter when there's more than one candidate
289 if (!followedWay) return;
292 if (followedWay.getNextNode(prevnode) == curnode) {
293 nextNode = followedWay.getNextNode(curnode);
294 } else if (followedWay.getNextNode(curnode) == prevnode){
295 nextNode = followedWay.getPrevNode(curnode);
296 } else if (followedWay.indexOfNode(curnode) > followedWay.indexOfNode(prevnode)) {
297 // The two nodes selected aren't actually consecutive. Make a half-hearted
298 // guess at which way to follow. Will be "incorrect" if the join in the loop
299 // is between the two points.
300 nextNode = followedWay.getNextNode(curnode);
302 nextNode = followedWay.getPrevNode(curnode);
304 if (!nextNode) return;
305 if (nextNode.hasParent(firstSelected)) return;
307 appendNode(nextNode as Node, MainUndoStack.getGlobalStack().addAction);
308 resetElastic(nextNode as Node);
310 controller.map.setHighlight(nextNode, { selectedway: true });
312 // recentre the map if the new lat/lon is offscreen
313 if (nextNode.lat > controller.map.edge_t ||
314 nextNode.lat < controller.map.edge_b ||
315 nextNode.lon < controller.map.edge_l ||
316 nextNode.lon > controller.map.edge_r) {
317 controller.map.moveMapFromLatLon(nextNode.lat, nextNode.lon);
321 override public function enterState():void {
324 Way(firstSelected).addEventListener(Connection.WAY_NODE_REMOVED, fixElastic);
325 Way(firstSelected).addEventListener(Connection.WAY_NODE_ADDED, fixElastic);
327 var node:Node = Way(firstSelected).getNode(editEnd ? Way(firstSelected).length-1 : 0);
328 var start:Point = new Point(node.lon, node.latp);
329 elastic = new Elastic(controller.map, start, start);
330 controller.setCursor(controller.pen);
331 Globals.vars.root.addDebug("**** -> "+this);
333 override public function exitState(newState:ControllerState):void {
334 Way(firstSelected).removeEventListener(Connection.WAY_NODE_REMOVED, fixElastic);
335 Way(firstSelected).removeEventListener(Connection.WAY_NODE_ADDED, fixElastic);
337 super.exitState(newState);
338 controller.setCursor(null);
339 elastic.removeSprites();
341 Globals.vars.root.addDebug("**** <- "+this);
343 override public function toString():String {