Merge branch 'master' into history
[potlatch2.git] / net / systemeD / halcyon / connection / Way.as
1 package net.systemeD.halcyon.connection {
2     import flash.geom.Point;
3     
4     import net.systemeD.halcyon.connection.actions.*;
5
6     public class Way extends Entity {
7         private var nodes:Array;
8                 private var edge_l:Number;
9                 private var edge_r:Number;
10                 private var edge_t:Number;
11                 private var edge_b:Number;
12                 public static var entity_type:String = 'way';
13
14         public function Way(connection:Connection, id:Number, version:uint, tags:Object, loaded:Boolean, nodes:Array, uid:Number = NaN, timestamp:String = null, user:String = null) {
15             super(connection, id, version, tags, loaded, uid, timestamp, user);
16             this.nodes = nodes;
17                         for each (var node:Node in nodes) { node.addParent(this); }
18                         calculateBbox();
19         }
20
21                 public function update(version:uint, tags:Object, loaded:Boolean, parentsLoaded:Boolean, nodes:Array, uid:Number = NaN, timestamp:String = null, user:String = null):void {
22                         var node:Node;
23                         for each (node in this.nodes) { node.removeParent(this); }
24                         updateEntityProperties(version,tags,loaded,parentsLoaded,uid,timestamp,user); this.nodes=nodes;
25                         for each (node in nodes) { node.addParent(this); }
26                         calculateBbox();
27                 }
28                 
29         public function get length():uint {
30             return nodes.length;
31         }
32
33                 private function calculateBbox():void {
34                         edge_l=999999; edge_r=-999999;
35                         edge_b=999999; edge_t=-999999;
36                         for each (var node:Node in nodes) { expandBbox(node); }
37                 }
38
39                 public function expandBbox(node:Node):void {
40                         edge_l=Math.min(edge_l,node.lon);
41                         edge_r=Math.max(edge_r,node.lon);
42                         edge_b=Math.min(edge_b,node.lat);
43                         edge_t=Math.max(edge_t,node.lat);
44                 }
45                 
46                 public override function within(left:Number,right:Number,top:Number,bottom:Number):Boolean {
47                         if (!edge_l ||
48                                 (edge_l<left   && edge_r<left  ) ||
49                             (edge_l>right  && edge_r>right ) ||
50                             (edge_b<bottom && edge_t<bottom) ||
51                             (edge_b>top    && edge_b>top   ) || deleted) { return false; }
52                         return true;
53                 }
54
55         public function getNode(index:uint):Node {
56             return nodes[index];
57         }
58
59         public function getFirstNode():Node {
60             return nodes[0];
61         }
62
63                 public function getLastNode():Node {
64                         return nodes[nodes.length-1];
65                 }
66                 
67                 /** Given one node, return the next in sequence, cycling around a loop if necessary. */
68                 // TODO make behave correctly for P-shaped topologies?
69                 public function getNextNode(node:Node):Node {
70                         // If the last node in a loop is selected, this behaves correctly.
71                     var i:uint = indexOfNode(node);
72                     if(i < length-1)
73                     return nodes[i+1];
74                 return null;
75                 // What should happen for very short lengths?      
76                 }
77         
78         // TODO make behave correctly for P-shaped topologies?
79         /** Given one node, return the previous, cycling around a loop if necessary. */
80         public function getPrevNode(node:Node):Node {
81             var i:uint = indexOfNode(node);
82             if(i > 0)
83                 return nodes[i-1];
84             if(i == 0 && isArea() )
85                 return nodes[nodes.length - 2]
86             return null;
87             // What should happen for very short lengths?      
88         }
89
90         public function insertNode(index:uint, node:Node, performAction:Function):void {
91                         if (index>0 && getNode(index-1)==node) return;
92                         if (index<nodes.length-1 && getNode(index)==node) return;
93                         performAction(new AddNodeToWayAction(this, node, nodes, index, false));
94         }
95
96         public function appendNode(node:Node, performAction:Function):uint {
97                         if (node!=getLastNode()) performAction(new AddNodeToWayAction(this, node, nodes, -1));
98             return nodes.length + 1;
99         }
100         
101         public function prependNode(node:Node, performAction:Function):uint {
102                         if (node!=getFirstNode()) performAction(new AddNodeToWayAction(this, node, nodes, 0));
103             return nodes.length + 1;
104         }
105         
106         // return the index of the Node, or -1 if not found
107         public function indexOfNode(node:Node):int {
108             return nodes.indexOf(node);
109         }
110
111                 public function hasOnceOnly(node:Node):Boolean {
112                         return nodes.indexOf(node)==nodes.lastIndexOf(node);
113                 }
114                 
115                 public function hasLockedNodes():Boolean {
116                         for each (var node:Node in nodes) {
117                                 if (node.locked) { return true; }
118                         }
119                         return false;
120                 }
121
122                 public function removeNode(node:Node, performAction:Function):void {
123                         performAction(new RemoveNodeFromWayAction(this, node, nodes));
124                 }
125
126         public function removeNodeByIndex(index:uint, performAction:Function, fireEvent:Boolean=true):void {
127             performAction(new RemoveNodeByIndexAction(this, nodes, index, fireEvent));
128         }
129
130                 public function sliceNodes(start:int,end:int):Array {
131                         return nodes.slice(start,end);
132                 }
133
134         public function deleteNodesFrom(start:int, performAction:Function):void {
135             for (var i:int=nodes.length-1; i>=start; i--) {
136               performAction(new RemoveNodeByIndexAction(this, nodes, i));
137             }
138             markDirty();
139         }
140
141                 /** Merges another way into this one, removing the other one. */
142                 public function mergeWith(way:Way,topos:int,frompos:int, performAction:Function):void {
143                         performAction(new MergeWaysAction(this, way, topos, frompos));
144                 }
145                 
146                 public function addToEnd(topos:int,node:Node, performAction:Function):void {
147                         if (topos==0) {
148                                 if (nodes[0]==node) { return; }
149                                 prependNode(node, performAction);
150                         } else {
151                                 if (nodes[nodes.length-1]==node) { return; }
152                                 appendNode(node, performAction);
153                         }
154                 }
155
156         public function reverseNodes(performAction:Function):void {
157             performAction(new ReverseNodesAction(this, nodes));
158         }
159
160                 
161                 /** Is a point within this way?
162                 * From http://as3.miguelmoraleda.com/2009/10/28/point-in-polygon-with-actionscript-3punto-dentro-de-un-poligono-con-actionscript-3/
163                 */
164
165                 public function pointWithin(lon:Number,lat:Number):Boolean {
166                         if (!isArea()) return false;
167                         
168                         var counter:uint = 0;
169                         var p1x:Number = nodes[0].lon;
170                         var p1y:Number = nodes[0].lat;
171                         var p2x:Number, p2y:Number;
172  
173                         for (var i:uint = 1; i <= length; i++) {
174                                 p2x = nodes[i % length].lon;
175                                 p2y = nodes[i % length].lat;
176                                 if (lat > Math.min(p1y, p2y)) {
177                                         if (lat <= Math.max(p1y, p2y)) {
178                                                 if (lon <= Math.max(p1x, p2x)) {
179                                                         if (p1y != p2y) {
180                                                                 var xinters:Number = (lat - p1y) * (p2x - p1x) / (p2y - p1y) + p1x;
181                                                                 if (p1x == p2x || lon <= xinters) counter++;
182                                                         }
183                                                 }
184                                         }
185                                 }
186                                 p1x = p2x;
187                                 p1y = p2y;
188                         }
189                         if (counter % 2 == 0) { return false; }
190                         else { return true; }
191                 }
192
193         /**
194          * Finds the 1st way segment which intersects the projected
195          * coordinate and adds the node to that segment. If snap is
196          * specified then the node is moved to exactly bisect the
197          * segment.
198          */
199         public function insertNodeAtClosestPosition(newNode:Node, isSnap:Boolean, performAction:Function):int {
200             var closestProportion:Number = 1;
201             var newIndex:uint = 0;
202             var nP:Point = new Point(newNode.lon, newNode.latp);
203             var snapped:Point = null;
204             
205             for ( var i:uint; i < length - 1; i++ ) {
206                 var node1:Node = getNode(i);
207                 var node2:Node = getNode(i+1);
208                 var p1:Point = new Point(node1.lon, node1.latp);
209                 var p2:Point = new Point(node2.lon, node2.latp);
210                 
211                 var directDist:Number = Point.distance(p1, p2);
212                 var viaNewDist:Number = Point.distance(p1, nP) + Point.distance(nP, p2);
213                         
214                 var proportion:Number = Math.abs(viaNewDist/directDist - 1);
215                 if ( proportion < closestProportion ) {
216                     newIndex = i+1;
217                     closestProportion = proportion;
218                     snapped = calculateSnappedPoint(p1, p2, nP);
219                 }
220             }
221             
222             // splice in new node
223             if ( isSnap ) {
224                 newNode.setLonLatp(snapped.x, snapped.y, performAction);
225             }
226             insertNode(newIndex, newNode, performAction);
227             return newIndex;
228         }
229         
230         private function calculateSnappedPoint(p1:Point, p2:Point, nP:Point):Point {
231             var w:Number = p2.x - p1.x;
232             var h:Number = p2.y - p1.y;
233             var u:Number = ((nP.x-p1.x) * w + (nP.y-p1.y) * h) / (w*w + h*h);
234             return new Point(p1.x + u*w, p1.y+u*h);
235         }
236         
237         public override function toString():String {
238             return "Way("+id+"@"+version+"): "+getTagList()+
239                      " "+nodes.map(function(item:Node,index:int, arr:Array):String {return item.id.toString();}).join(",");
240         }
241
242                 public function isArea():Boolean {
243                         if (nodes.length==0) { return false; }
244                         return (nodes[0].id==nodes[nodes.length-1].id && nodes.length>2);
245                 }
246                 
247                 public function endsWith(node:Node):Boolean {
248                         return (nodes[0]==node || nodes[nodes.length-1]==node);
249                 }
250                 
251                 public override function remove(performAction:Function):void {
252                         performAction(new DeleteWayAction(this, setDeletedState, nodes));
253                 }
254
255                 public override function nullify():void {
256                         nullifyEntity();
257                         nodes=[];
258                         edge_l=edge_r=edge_t=edge_b=NaN;
259                 }
260                 
261                 public function get clockwise():Boolean {
262                         var lowest:uint=0;
263                         var xmin:Number=-999999; var ymin:Number=-999999;
264                         for (var i:uint=0; i<nodes.length; i++) {
265                                 if      (nodes[i].latp> ymin) { lowest=i; xmin=nodes[i].lon; ymin=nodes[i].latp; }
266                                 else if (nodes[i].latp==ymin
267                                           && nodes[i].lon > xmin) { lowest=i; xmin=nodes[i].lon; ymin=nodes[i].latp; }
268                         }
269                         return (this.onLeft(lowest)>0);
270                 }
271                 
272                 private function onLeft(j:uint):Number {
273                         var left:Number=0;
274                         var i:int, k:int;
275                         if (nodes.length>=3) {
276                                 i=j-1; if (i==-1) { i=nodes.length-2; }
277                                 k=j+1; if (k==nodes.length) { k=1; }
278                                 left=((nodes[j].lon-nodes[i].lon) * (nodes[k].latp-nodes[i].latp) -
279                                           (nodes[k].lon-nodes[i].lon) * (nodes[j].latp-nodes[i].latp));
280                         }
281                         return left;
282                 }
283
284         public function get angle():Number {
285             var dx:Number = nodes[nodes.length-1].lon - nodes[0].lon;
286             var dy:Number = nodes[nodes.length-1].latp - nodes[0].latp;
287             if (dx != 0 || dy != 0) {
288                 return Math.atan2(dx,dy)*(180/Math.PI);
289             } else {
290                 return 0;
291             }
292         }
293
294                 internal override function isEmpty():Boolean {
295                         return (deleted || (nodes.length==0));
296                 }
297
298                 public override function getType():String {
299                         return 'way';
300                 }
301                 
302                 public override function isType(str:String):Boolean {
303                         if (str=='way') return true;
304                         if (str=='line' && !isArea()) return true;
305                         if (str=='area' &&  isArea()) return true;
306                         return false;
307                 }
308                 
309                 /** Whether the way has a loop that joins back midway along its length */
310                 public function isPShape():Boolean {
311                         return getFirstNode() != getLastNode() && (!hasOnceOnly(getFirstNode()) || !hasOnceOnly(getLastNode()) );
312                 }
313                 
314                 /** Given a P-shaped way, return the index of midway node that one end connects back to. */
315                 public function getPJunctionNodeIndex():uint {
316                         if (isPShape()) {
317                             if (hasOnceOnly(getFirstNode())) {
318                                 // nodes[0] is the free end
319                                 return nodes.indexOf(getLastNode());
320                             } else {
321                                 // nodes[0] is in the loop
322                                 return nodes.lastIndexOf(getFirstNode());
323                             }
324                         }
325                         return null;
326                 }
327
328                 public function intersects(left:Number,right:Number,top:Number,bottom:Number):Boolean {
329                         // simple test first: are any nodes contained?
330                         for (var i:uint=0; i<nodes.length; i++) {
331                                 if (nodes[i].within(left,right,top,bottom)) return true;
332                         }
333                         // more complex test: do any segments cross?
334                         for (i=0; i<nodes.length-1; i++) {
335                                 if (lineIntersectsRectangle(
336                                         nodes[i  ].lon, nodes[i  ].lat,
337                                         nodes[i+1].lon, nodes[i+1].lat,
338                                         left,right,top,bottom)) return true;
339                         }
340                         return false;
341                 }
342                 
343                 private function lineIntersectsRectangle(x0:Number, y0:Number, x1:Number, y1:Number, l:Number, r:Number, b:Number, t:Number):Boolean {
344                         // from http://sebleedelisle.com/2009/05/super-fast-trianglerectangle-intersection-test/
345                         // note that t and b are transposed above because we're dealing with lat (top=90), not AS3 pixels (top=0)
346                         var m:Number = (y1-y0) / (x1-x0);
347                         var c:Number = y0 -(m*x0);
348                         var top_intersection:Number, bottom_intersection:Number;
349                         var toptrianglepoint:Number, bottomtrianglepoint:Number;
350
351                         if (m>0) {
352                                 top_intersection = (m*l  + c);
353                                 bottom_intersection = (m*r  + c);
354                         } else {
355                                 top_intersection = (m*r  + c);
356                                 bottom_intersection = (m*l  + c);
357                         }
358
359                         if (y0<y1) {
360                                 toptrianglepoint = y0;
361                                 bottomtrianglepoint = y1;
362                         } else {
363                                 toptrianglepoint = y1;
364                                 bottomtrianglepoint = y0;
365                         }
366
367                         var topoverlap:Number = top_intersection>toptrianglepoint ? top_intersection : toptrianglepoint;
368                         var botoverlap:Number = bottom_intersection<bottomtrianglepoint ? bottom_intersection : bottomtrianglepoint;
369                         return (topoverlap<botoverlap) && (!((botoverlap<t) || (topoverlap>b)));
370                 }
371
372
373     }
374 }