fix way splitting and node/UI renumbering
[potlatch2.git] / net / systemeD / halcyon / connection / Way.as
1 package net.systemeD.halcyon.connection {
2     import flash.geom.Point;
3         import net.systemeD.halcyon.Globals;
4
5     public class Way extends Entity {
6         private var nodes:Array;
7                 public static var entity_type:String = 'way';
8
9         public function Way(id:Number, version:uint, tags:Object, loaded:Boolean, nodes:Array) {
10             super(id, version, tags, loaded);
11             this.nodes = nodes;
12                         for each (var node:Node in nodes) { node.addParent(this); }
13         }
14
15                 public function update(version:uint, tags:Object, loaded:Boolean, nodes:Array):void {
16                         var node:Node;
17                         for each (node in this.nodes) { node.removeParent(this); }
18                         updateEntityProperties(version,tags,loaded); this.nodes=nodes;
19                         for each (node in nodes) { node.addParent(this); }
20                 }
21                 
22         public function get length():uint {
23             return nodes.length;
24         }
25         
26         public function getNode(index:uint):Node {
27             return nodes[index];
28         }
29
30                 public function getLastNode():Node {
31                         return nodes[nodes.length-1];
32                 }
33
34         public function insertNode(index:uint, node:Node):void {
35                         node.addParent(this);
36             nodes.splice(index, 0, node);
37             markDirty();
38             dispatchEvent(new WayNodeEvent(Connection.WAY_NODE_ADDED, node, this, index));
39         }
40
41         public function appendNode(node:Node):uint {
42                         node.addParent(this);
43             nodes.push(node);
44             markDirty();
45             dispatchEvent(new WayNodeEvent(Connection.WAY_NODE_ADDED, node, this, nodes.length - 1));
46             return nodes.length;
47         }
48         
49         public function prependNode(node:Node):uint {
50                         node.addParent(this);
51             nodes.unshift(node);
52             markDirty();
53             dispatchEvent(new WayNodeEvent(Connection.WAY_NODE_ADDED, node, this, 0));
54             return nodes.length;
55         }
56         
57         public function indexOfNode(node:Node):uint {
58             return nodes.indexOf(node);
59         }
60
61                 public function removeNode(node:Node):void {
62                         var i:int;
63                         while ((i=nodes.indexOf(node))>-1) {
64                                 nodes.splice(i,1);
65                 dispatchEvent(new WayNodeEvent(Connection.WAY_NODE_REMOVED, node, this, i));
66                         }
67                         node.removeParent(this);
68                         markDirty();
69                 }
70
71         public function removeNodeByIndex(index:uint):void {
72             var removed:Array=nodes.splice(index, 1);
73                         if (nodes.indexOf(removed[0])==-1) { removed[0].removeParent(this); }
74                         markDirty();
75             dispatchEvent(new WayNodeEvent(Connection.WAY_NODE_REMOVED, removed[0], this, index));
76         }
77
78                 public function sliceNodes(start:int,end:int):Array {
79                         return nodes.slice(start,end);
80                 }
81
82                 public function deleteNodesFrom(start:int):void {
83                         for (var i:int=start; i<nodes.length; i++) {
84                                 nodes[i].removeParent(this);
85                         }
86                         nodes.splice(start);
87                         markDirty();
88                 }
89
90                 public function mergeWith(way:Way,topos:int,frompos:int):void {
91                         var i:int;
92                         suspend();
93
94                         // merge relations
95                         for each (var r:Relation in way.parentRelations) {
96                                 // ** needs to copy roles as well
97                                 if (r.findEntityMemberIndex(this)==-1) {
98                                         r.appendMember(new RelationMember(this, ''));
99                                 }
100                         }
101
102                         // merge tags
103                         var t:Object=way.getTagsHash();
104                         for (var k:String in t) {
105                                 if (getTag(k) && getTag(k)!=t[k]) {
106                                         setTag(k,getTag(k)+"; "+t[k]);
107                                         // ** send a warning about tags not matching
108                                 } else {
109                                         setTag(k,t[k]);
110                                 }
111                         }
112
113                         // merge nodes
114                         if (frompos==0) { for (i=0; i<way.length;    i++) { addToEnd(topos,way.getNode(i)); } }
115                                            else { for (i=way.length-1; i>=0; i--) { addToEnd(topos,way.getNode(i)); } }
116
117                         // delete way
118                         way.remove();
119                         resume();
120                 }
121                 
122                 private function addToEnd(topos:int,node:Node):void {
123                         if (topos==0) {
124                                 if (nodes[0]==node) { return; }
125                                 prependNode(node);
126                         } else {
127                                 if (nodes[nodes.length-1]==node) { return; }
128                                 appendNode(node);
129                         }
130                 }
131
132
133
134         /**
135          * Finds the 1st way segment which intersects the projected
136          * coordinate and adds the node to that segment. If snap is
137          * specified then the node is moved to exactly bisect the
138          * segment.
139          */
140         public function insertNodeAtClosestPosition(newNode:Node, isSnap:Boolean):int {
141             var closestProportion:Number = 1;
142             var newIndex:uint = 0;
143             var nP:Point = new Point(newNode.lon, newNode.latp);
144             var snapped:Point = null;
145             
146             for ( var i:uint; i < length - 1; i++ ) {
147                 var node1:Node = getNode(i);
148                 var node2:Node = getNode(i+1);
149                 var p1:Point = new Point(node1.lon, node1.latp);
150                 var p2:Point = new Point(node2.lon, node2.latp);
151                 
152                 var directDist:Number = Point.distance(p1, p2);
153                 var viaNewDist:Number = Point.distance(p1, nP) + Point.distance(nP, p2);
154                         
155                 var proportion:Number = Math.abs(viaNewDist/directDist - 1);
156                 if ( proportion < closestProportion ) {
157                     newIndex = i+1;
158                     closestProportion = proportion;
159                     snapped = calculateSnappedPoint(p1, p2, nP);
160                 }
161             }
162             
163             // splice in new node
164             if ( isSnap ) {
165                 newNode.latp = snapped.y;
166                 newNode.lon = snapped.x;
167             }
168             insertNode(newIndex, newNode);
169             return newIndex;
170         }
171         
172         private function calculateSnappedPoint(p1:Point, p2:Point, nP:Point):Point {
173             var w:Number = p2.x - p1.x;
174             var h:Number = p2.y - p1.y;
175             var u:Number = ((nP.x-p1.x) * w + (nP.y-p1.y) * h) / (w*w + h*h);
176             return new Point(p1.x + u*w, p1.y+u*h);
177         }
178         
179         public override function toString():String {
180             return "Way("+id+"@"+version+"): "+getTagList()+
181                      " "+nodes.map(function(item:Node,index:int, arr:Array):String {return item.id.toString();}).join(",");
182         }
183
184                 public function isArea():Boolean {
185                         return (nodes[0].id==nodes[nodes.length-1].id && nodes.length>2);
186                 }
187                 
188                 public function get clockwise():Boolean {
189                         var lowest:uint=0;
190                         var xmin:Number=-999999; var ymin:Number=-999999;
191                         for (var i:uint=0; i<nodes.length; i++) {
192                                 if      (nodes[i].latp> ymin) { lowest=i; xmin=nodes[i].lon; ymin=nodes[i].latp; }
193                                 else if (nodes[i].latp==ymin
194                                           && nodes[i].lon > xmin) { lowest=i; xmin=nodes[i].lon; ymin=nodes[i].latp; }
195                         }
196                         return (this.onLeft(lowest)>0);
197                 }
198                 
199                 private function onLeft(j:uint):Number {
200                         var left:Number=0;
201                         var i:int, k:int;
202                         if (nodes.length>=3) {
203                                 i=j-1; if (i==-1) { i=nodes.length-2; }
204                                 k=j+1; if (k==nodes.length) { k=1; }
205                                 left=((nodes[j].lon-nodes[i].lon) * (nodes[k].latp-nodes[i].latp) -
206                                           (nodes[k].lon-nodes[i].lon) * (nodes[j].latp-nodes[i].latp));
207                         }
208                         return left;
209                 }
210
211                 public override function remove():void {
212                         var node:Node;
213                         suspend();
214                         removeFromParents();
215                         while (nodes.length) { 
216                                 node=nodes.pop();
217                                 dispatchEvent(new WayNodeEvent(Connection.WAY_NODE_REMOVED, node, this, 0));
218                                 node.removeParent(this);
219                                 if (!node.hasParents) { node.remove(); }
220                         }
221                         deleted=true;
222             dispatchEvent(new EntityEvent(Connection.WAY_DELETED, this));
223                         resume();
224                 }
225
226                 internal override function isEmpty():Boolean {
227                         return (deleted || (nodes.length==0));
228                 }
229
230                 public override function getType():String {
231                         return 'way';
232                 }
233     }
234
235 }