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.MapPaint;
12 public class DrawWay extends SelectedWay {
13 private var elastic:Elastic;
14 private var editEnd:Boolean; // if true, we're drawing from node[n-1], else "backwards" from node[0]
15 private var leaveNodeSelected:Boolean;
16 private var lastClick:Entity=null;
17 private var lastClickTime:Date;
18 private var hoverEntity:Entity; // keep track of the currently rolled-over object, because
19 // Flash can fire a mouseDown from the map even if you
20 // haven't rolled out of the way
22 public function DrawWay(way:Way, editEnd:Boolean, leaveNodeSelected:Boolean) {
24 this.editEnd = editEnd;
25 this.leaveNodeSelected = leaveNodeSelected;
27 // drawing new way, so keep track of click in case creating a POI
28 lastClick=way.getNode(0);
29 lastClickTime=new Date();
33 override public function processMouseEvent(event:MouseEvent, entity:Entity):ControllerState {
36 var paint:MapPaint = getMapPaint(DisplayObject(event.target));
37 var isBackground:Boolean = paint && paint.isBackground;
39 if (entity == null && hoverEntity) { entity=hoverEntity; }
40 var focus:Entity = getTopLevelFocusEntity(entity);
42 if ( event.type == MouseEvent.MOUSE_UP ) {
43 controller.map.mouseUpHandler(); // in case you're still in the drag-tolerance zone, and mouse up over something.
44 if ( entity == null || isBackground ) { // didn't hit anything: extend the way by one node.
45 node = createAndAddNode(event, MainUndoStack.getGlobalStack().addAction);
46 editableLayer.setHighlight(node, { selectedway: true });
47 editableLayer.setPurgable([node], false);
50 controller.updateSelectionUIWithoutTagChange();
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(
59 editableLayer.connection,
61 controller.map.coord2lat(event.localY),
62 controller.map.coord2lon(event.localX));
63 MainUndoStack.getGlobalStack().addAction(newPoiAction);
64 return new SelectedPOINode(newPoiAction.getNode());
65 } else if (Way(firstSelected).length==1) {
66 // It's not a poi, but they've double-clicked or clicked-twice the first node - do nothing
69 // double-click at end of way
72 } else if (entity==lastClick) {
73 // clicked slowly on the end node - do nothing
76 // hit a node, add it to this way and carry on
77 appendNode(entity as Node, MainUndoStack.getGlobalStack().addAction);
79 editableLayer.setHighlightOnNodes(focus as Way, { hoverway: false });
81 editableLayer.setHighlight(entity, { selectedway: true });
82 resetElastic(entity as Node);
84 if (Way(firstSelected).getNode(0)==Way(firstSelected).getLastNode()) {
85 // the node just hit completes a loop, so stop drawing.
86 return new SelectedWay(firstSelected as Way);
89 } else if ( entity is Way ) {
90 if (entity==firstSelected) {
91 // add junction node - self-intersecting way
92 var lat:Number = controller.map.coord2lat(event.localY);
93 var lon:Number = controller.map.coord2lon(event.localX);
94 var undo:CompositeUndoableAction = new CompositeUndoableAction("Insert node");
95 node = firstSelected.connection.createNode({}, lat, lon, undo.push);
96 Way(firstSelected).insertNodeAtClosestPosition(node, true, undo.push);
97 appendNode(node,undo.push);
98 MainUndoStack.getGlobalStack().addAction(undo);
100 // add junction node - another way
101 var jnct:CompositeUndoableAction = new CompositeUndoableAction("Junction Node");
102 node = createAndAddNode(event, jnct.push);
103 Way(entity).insertNodeAtClosestPosition(node, true, jnct.push);
104 MainUndoStack.getGlobalStack().addAction(jnct);
105 editableLayer.setHighlight(node, { selectedway: true });
106 editableLayer.setPurgable([node], false);
110 editableLayer.setHighlightOnNodes(entity as Way, { hoverway: false });
111 editableLayer.setHighlightOnNodes(firstSelected as Way, { selectedway: true });
113 lastClickTime=new Date();
114 } else if ( event.type == MouseEvent.MOUSE_MOVE && elastic ) {
115 // mouse is roaming around freely
117 controller.map.coord2lon(event.localX),
118 controller.map.coord2latp(event.localY));
120 } else if ( event.type == MouseEvent.ROLL_OVER && !isBackground ) {
121 // mouse has floated over something
122 if (focus is Way && focus!=firstSelected) {
123 // floating over another way, highlight its nodes
125 editableLayer.setHighlightOnNodes(focus as Way, { hoverway: true });
127 // set cursor depending on whether we're floating over the start of this way,
128 // another random node, a possible junction...
129 if (entity is Node && focus is Way && Way(focus).endsWith(Node(entity))) {
130 if (focus==firstSelected) { controller.setCursor(controller.pen_so); }
131 else { controller.setCursor(controller.pen_o); }
132 } else if (entity is Node) {
133 controller.setCursor(controller.pen_x);
135 controller.setCursor(controller.pen_plus);
137 } else if ( event.type == MouseEvent.MOUSE_OUT && !isBackground ) {
138 if (focus is Way && entity!=firstSelected) {
140 editableLayer.setHighlightOnNodes(focus as Way, { hoverway: false });
141 // ** We could do with an optional way of calling WayUI.redraw to only do the nodes, which would be a
142 // useful optimisation.
144 controller.setCursor(controller.pen);
150 protected function resetElastic(node:Node):void {
151 elastic.start = new Point(node.lon, node.latp);
152 elastic.end = new Point(controller.map.coord2lon(controller.map.mouseX),
153 controller.map.coord2latp(controller.map.mouseY));
156 /* Fix up the elastic after a WayNode event - e.g. triggered by undo */
157 private function fixElastic(event:Event):void {
158 if (firstSelected == null) return;
161 node = Way(firstSelected).getLastNode();
163 node = Way(firstSelected).getNode(0);
165 if (node) { //maybe selectedWay doesn't have any nodes left
166 elastic.start = new Point(node.lon, node.latp);
170 override public function processKeyboardEvent(event:KeyboardEvent):ControllerState {
171 switch (event.keyCode) {
172 case Keyboard.ENTER: return keyExitDrawing();
173 case Keyboard.ESCAPE: return keyExitDrawing();
174 case Keyboard.DELETE:
175 case Keyboard.BACKSPACE:
176 case 189: /* minus */ return backspaceNode(MainUndoStack.getGlobalStack().addAction);
177 case 82: /* R */ repeatTags(firstSelected); return this;
178 case 70: /* F */ followWay(); return this;
180 var cs:ControllerState = sharedKeyboardEvents(event);
181 return cs ? cs : this;
185 protected function keyExitDrawing():ControllerState {
186 var cs:ControllerState=stopDrawing();
187 if (selectedWay.length==1) {
188 if (MainUndoStack.getGlobalStack().undoIfAction(BeginWayAction)) {
189 return new NoSelection();
196 protected function stopDrawing():ControllerState {
198 editableLayer.setHighlightOnNodes(hoverEntity as Way, { hoverway: false });
202 if ( leaveNodeSelected ) {
203 return new SelectedWayNode(firstSelected as Way, editEnd ? Way(firstSelected).length-1 : 0);
205 return new SelectedWay(firstSelected as Way);
209 public function createAndAddNode(event:MouseEvent, performAction:Function):Node {
210 var undo:CompositeUndoableAction = new CompositeUndoableAction("Add node");
212 var lat:Number = controller.map.coord2lat(event.localY);
213 var lon:Number = controller.map.coord2lon(event.localX);
214 var node:Node = firstSelected.connection.createNode({}, lat, lon, undo.push);
215 appendNode(node, undo.push);
221 protected function appendNode(node:Node, performAction:Function):void {
223 Way(firstSelected).appendNode(node, performAction);
225 Way(firstSelected).insertNode(0, node, performAction);
228 protected function backspaceNode(performAction:Function):ControllerState {
229 if (selectedWay.length==1) return keyExitDrawing();
232 var undo:CompositeUndoableAction = new CompositeUndoableAction("Remove node");
234 var state:ControllerState;
237 node=Way(firstSelected).getLastNode();
238 Way(firstSelected).removeNodeByIndex(Way(firstSelected).length-1, undo.push);
239 newDraw=Way(firstSelected).length-2;
241 node=Way(firstSelected).getNode(0);
242 Way(firstSelected).removeNodeByIndex(0, undo.push);
245 // Only actually delete the node if it has no other tags, and is not part of other ways (or part of this way twice)
246 if (node.numParentWays==1 && Way(firstSelected).hasOnceOnly(node) && !node.hasInterestingTags()) {
247 editableLayer.setPurgable([node], true);
248 node.connection.unregisterPOI(node);
249 node.remove(undo.push);
252 if (newDraw>=0 && newDraw<=Way(firstSelected).length-2) {
253 var mouse:Point = new Point(Way(firstSelected).getNode(newDraw).lon, Way(firstSelected).getNode(newDraw).latp);
254 elastic.start = mouse;
257 Way(firstSelected).remove(undo.push);
258 state = new NoSelection();
263 if(!node.isDeleted()) { // i.e. was junction with another way (or is now POI)
264 editableLayer.setHighlight(node, {selectedway: false});
269 /** Extends the current way by "following" an existing way, after the user has already selected two nodes in a row.
270 If drawing way has at least two nodes, and both belong to another way, and those ways are the same,
271 then find the next node, add that node, update screen and scroll the new node into shot if necessary.
272 TODO: add a bit of feedback (FloatingAlert?) when following can't be carried out for some reason. */
273 protected function followWay():void {
276 if (Way(firstSelected).length < 2) return;
279 curnode = Way(firstSelected).getLastNode();
280 prevnode = Way(firstSelected).getNode(Way(firstSelected).length-2);
282 curnode = Way(firstSelected).getNode(0);
283 prevnode = Way(firstSelected).getNode(1);
285 if (curnode.numParentWays <2 || prevnode.numParentWays <2) return;
288 for each (var way:Way in curnode.parentWays) {
289 if (way!=firstSelected && prevnode.hasParent(way))
290 followedWay = way; // FIXME: could be smarter when there's more than one candidate
292 if (!followedWay) return;
295 if (followedWay.getNextNode(prevnode) == curnode) {
296 nextNode = followedWay.getNextNode(curnode);
297 } else if (followedWay.getNextNode(curnode) == prevnode){
298 nextNode = followedWay.getPrevNode(curnode);
299 } else if (followedWay.indexOfNode(curnode) > followedWay.indexOfNode(prevnode)) {
300 // The two nodes selected aren't actually consecutive. Make a half-hearted
301 // guess at which way to follow. Will be "incorrect" if the join in the loop
302 // is between the two points.
303 nextNode = followedWay.getNextNode(curnode);
305 nextNode = followedWay.getPrevNode(curnode);
307 if (!nextNode) return;
308 if (nextNode.hasParent(firstSelected) && !(firstSelected as Way).hasOnceOnly(curnode)) return;
310 appendNode(nextNode as Node, MainUndoStack.getGlobalStack().addAction);
311 resetElastic(nextNode as Node);
313 editableLayer.setHighlight(nextNode, { selectedway: true });
315 // recentre the map if the new lat/lon is offscreen
316 controller.map.scrollIfNeeded(nextNode.lat,nextNode.lon);
319 override public function enterState():void {
322 Way(firstSelected).addEventListener(Connection.WAY_NODE_REMOVED, fixElastic);
323 Way(firstSelected).addEventListener(Connection.WAY_NODE_ADDED, fixElastic);
325 var node:Node = Way(firstSelected).getNode(editEnd ? Way(firstSelected).length-1 : 0);
326 var start:Point = new Point(node.lon, node.latp);
327 elastic = new Elastic(controller.map, start, start);
328 controller.setCursor(controller.pen);
330 override public function exitState(newState:ControllerState):void {
331 Way(firstSelected).removeEventListener(Connection.WAY_NODE_REMOVED, fixElastic);
332 Way(firstSelected).removeEventListener(Connection.WAY_NODE_ADDED, fixElastic);
334 super.exitState(newState);
335 controller.setCursor(null);
336 elastic.removeSprites();
339 override public function toString():String {