Merge branch 'master' of github.com:systemed/potlatch2
[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                 public function get nodeList():Array {
34                         var arr:Array=[];
35                         for each (var node:Node in nodes) { arr.push(node.id); }
36                         return arr;
37                 }
38
39                 private function calculateBbox():void {
40                         edge_l=999999; edge_r=-999999;
41                         edge_b=999999; edge_t=-999999;
42                         for each (var node:Node in nodes) { expandBbox(node); }
43                 }
44
45                 public function expandBbox(node:Node):void {
46                         edge_l=Math.min(edge_l,node.lon);
47                         edge_r=Math.max(edge_r,node.lon);
48                         edge_b=Math.min(edge_b,node.lat);
49                         edge_t=Math.max(edge_t,node.lat);
50                 }
51                 
52                 public override function within(left:Number,right:Number,top:Number,bottom:Number):Boolean {
53                         if (!edge_l ||
54                                 (edge_l<left   && edge_r<left  ) ||
55                             (edge_l>right  && edge_r>right ) ||
56                             (edge_b<bottom && edge_t<bottom) ||
57                             (edge_b>top    && edge_b>top   ) || deleted) { return false; }
58                         return true;
59                 }
60
61         public function getNode(index:uint):Node {
62             return nodes[index];
63         }
64
65         public function getFirstNode():Node {
66             return nodes[0];
67         }
68
69                 public function getLastNode():Node {
70                         return nodes[nodes.length-1];
71                 }
72                 
73                 /** Given one node, return the next in sequence, cycling around a loop if necessary. */
74                 // TODO make behave correctly for P-shaped topologies?
75                 public function getNextNode(node:Node):Node {
76                         // If the last node in a loop is selected, this behaves correctly.
77                     var i:uint = indexOfNode(node);
78                     if(i < length-1)
79                     return nodes[i+1];
80                 return null;
81                 // What should happen for very short lengths?      
82                 }
83         
84         // TODO make behave correctly for P-shaped topologies?
85         /** Given one node, return the previous, cycling around a loop if necessary. */
86         public function getPrevNode(node:Node):Node {
87             var i:uint = indexOfNode(node);
88             if(i > 0)
89                 return nodes[i-1];
90             if(i == 0 && isArea() )
91                 return nodes[nodes.length - 2]
92             return null;
93             // What should happen for very short lengths?      
94         }
95
96         public function insertNode(index:uint, node:Node, performAction:Function):void {
97                         if (index>0 && getNode(index-1)==node) return;
98                         if (index<nodes.length-1 && getNode(index)==node) return;
99                         performAction(new AddNodeToWayAction(this, node, nodes, index, false));
100         }
101
102         public function appendNode(node:Node, performAction:Function):uint {
103                         if (node!=getLastNode()) performAction(new AddNodeToWayAction(this, node, nodes, -1));
104             return nodes.length + 1;
105         }
106         
107         public function prependNode(node:Node, performAction:Function):uint {
108                         if (node!=getFirstNode()) performAction(new AddNodeToWayAction(this, node, nodes, 0));
109             return nodes.length + 1;
110         }
111         
112         // return the index of the Node, or -1 if not found
113         public function indexOfNode(node:Node):int {
114             return nodes.indexOf(node);
115         }
116
117                 public function hasOnceOnly(node:Node):Boolean {
118                         return nodes.indexOf(node)==nodes.lastIndexOf(node);
119                 }
120                 
121                 public function hasLockedNodes():Boolean {
122                         for each (var node:Node in nodes) {
123                                 if (node.locked) { return true; }
124                         }
125                         return false;
126                 }
127
128                 public function removeNode(node:Node, performAction:Function):void {
129                         performAction(new RemoveNodeFromWayAction(this, node, nodes));
130                 }
131
132         public function removeNodeByIndex(index:uint, performAction:Function, fireEvent:Boolean=true):void {
133             performAction(new RemoveNodeByIndexAction(this, nodes, index, fireEvent));
134         }
135
136                 public function sliceNodes(start:int,end:int):Array {
137                         return nodes.slice(start,end);
138                 }
139
140         public function deleteNodesFrom(start:int, performAction:Function):void {
141             for (var i:int=nodes.length-1; i>=start; i--) {
142               performAction(new RemoveNodeByIndexAction(this, nodes, i));
143             }
144             markDirty();
145         }
146
147                 override public function getRelatedEntities():Array {
148                         var multis:Array = findParentRelationsOfType('multipolygon');
149                         var related:Array = [this];
150                         var wayHasInteresting:Boolean = hasInterestingTags();
151                         for each (var multi:Relation in multis) {
152                                 // if an old-style multipolygon, add to end of list
153                                 if (wayHasInteresting && multi.hasKeysOtherThanType()) { related.push(multi); }
154                                 // if a new-style multipolygon, add to start of list
155                                 else { related.unshift(multi); }
156                         }
157                         return related;
158                 }
159
160                 /** Merges another way into this one, removing the other one. */
161                 public function mergeWith(way:Way,topos:int,frompos:int, performAction:Function):void {
162                         performAction(new MergeWaysAction(this, way, topos, frompos));
163                 }
164                 
165                 public function addToEnd(topos:int,node:Node, performAction:Function):void {
166                         if (topos==0) {
167                                 if (nodes[0]==node) { return; }
168                                 prependNode(node, performAction);
169                         } else {
170                                 if (nodes[nodes.length-1]==node) { return; }
171                                 appendNode(node, performAction);
172                         }
173                 }
174
175         public function reverseNodes(performAction:Function):void {
176             performAction(new ReverseNodesAction(this, nodes));
177         }
178         
179         /** Check for, and remove, consecutive series of the same node */ 
180         public function removeRepeatedNodes(performAction:Function):void {
181                 var n: Node = nodes[0];
182                 for (var i:int = 1; i < nodes.length; i++) {
183                         if (nodes[i] == nodes[i-1]) {
184                                 removeNodeByIndex(i, performAction);
185                         }
186                 }
187         } 
188
189                 
190                 /** Is a point within this way?
191                 * From http://as3.miguelmoraleda.com/2009/10/28/point-in-polygon-with-actionscript-3punto-dentro-de-un-poligono-con-actionscript-3/
192                 */
193
194                 public function pointWithin(lon:Number,lat:Number):Boolean {
195                         if (!isArea()) return false;
196                         
197                         var counter:uint = 0;
198                         var p1x:Number = nodes[0].lon;
199                         var p1y:Number = nodes[0].lat;
200                         var p2x:Number, p2y:Number;
201  
202                         for (var i:uint = 1; i <= length; i++) {
203                                 p2x = nodes[i % length].lon;
204                                 p2y = nodes[i % length].lat;
205                                 if (lat > Math.min(p1y, p2y)) {
206                                         if (lat <= Math.max(p1y, p2y)) {
207                                                 if (lon <= Math.max(p1x, p2x)) {
208                                                         if (p1y != p2y) {
209                                                                 var xinters:Number = (lat - p1y) * (p2x - p1x) / (p2y - p1y) + p1x;
210                                                                 if (p1x == p2x || lon <= xinters) counter++;
211                                                         }
212                                                 }
213                                         }
214                                 }
215                                 p1x = p2x;
216                                 p1y = p2y;
217                         }
218                         if (counter % 2 == 0) { return false; }
219                         else { return true; }
220                 }
221
222                 /**
223                  * Finds the 1st way segment which intersects the projected
224                  * coordinate and adds the node to that segment. If snap is
225                  * specified then the node is moved to exactly bisect the
226                  * segment.
227                  */
228                 public function insertNodeAtClosestPosition(newNode:Node, isSnap:Boolean, performAction:Function):int {
229                         var o:Object=indexOfClosestNode(newNode.lon, newNode.latp);
230                         if (isSnap) { newNode.setLonLatp(o.snapped.x, o.snapped.y, performAction); }
231                         insertNode(o.index, newNode, performAction);
232                         return o.index;
233                 }
234
235                 /* Variant of insertNodeAtClosestPosition that will move an existing node if available,
236                    rather than creating a new one. Used for 'improve way accuracy' click.
237                    
238                    Ideally, rather than using 0.15/0.85, we should be sensitive to actual distance and to zoom level.
239                    Could maybe also do with being bent to fix the issue at concave angles. */
240                 public function insertNodeOrMoveExisting(lat:Number, lon:Number, performAction:Function):void {
241                         var latp:Number=Node.lat2latp(lat);
242                         var o:Object=distanceFromWay2(lon,latp);
243                         if (o.proportion<0.15) {
244                                 nodes[o.index-1].setLatLon(lat,lon,performAction);
245                         } else if (o.proportion>0.85) {
246                                 nodes[o.index  ].setLatLon(lat,lon,performAction);
247                         } else {
248                                 var node:Node = connection.createNode({}, lat, lon, performAction);
249                                 insertNode(o.index, node, performAction);
250                         }
251                 }
252
253                 /* Find which node is closest to a given lat/lon. */
254                 private function indexOfClosestNode(lon:Number, latp:Number):Object {
255             var closestProportion:Number = Infinity;
256             var newIndex:uint = 0;
257             var nP:Point = new Point(lon, latp);
258             var snapped:Point = null;
259             
260             for ( var i:uint; i < length - 1; i++ ) {
261                 var node1:Node = getNode(i);
262                 var node2:Node = getNode(i+1);
263                 var p1:Point = new Point(node1.lon, node1.latp);
264                 var p2:Point = new Point(node2.lon, node2.latp);
265                 
266                 var directDist:Number = Point.distance(p1, p2);
267                 var viaNewDist:Number = Point.distance(p1, nP) + Point.distance(nP, p2);
268                         
269                 var proportion:Number = Math.abs(viaNewDist/directDist - 1);
270                 if ( proportion < closestProportion ) {
271                     newIndex = i+1;
272                     closestProportion = proportion;
273                     snapped = calculateSnappedPoint(p1, p2, nP);
274                 }
275             }
276             return { index: newIndex, snapped: snapped };
277         }
278         
279         private function calculateSnappedPoint(p1:Point, p2:Point, nP:Point):Point {
280             var w:Number = p2.x - p1.x;
281             var h:Number = p2.y - p1.y;
282             var u:Number = ((nP.x-p1.x) * w + (nP.y-p1.y) * h) / (w*w + h*h);
283             return new Point(p1.x + u*w, p1.y+u*h);
284         }
285         
286         public override function toString():String {
287             return "Way("+id+"@"+version+"): "+getTagList()+
288                      " "+nodes.map(function(item:Node,index:int, arr:Array):String {return item.id.toString();}).join(",");
289         }
290
291                 public function isArea():Boolean {
292                         if (nodes.length==0) { return false; }
293                         return (nodes[0].id==nodes[nodes.length-1].id && nodes.length>2);
294                 }
295                 
296                 public function endsWith(node:Node):Boolean {
297                         return (nodes[0]==node || nodes[nodes.length-1]==node);
298                 }
299                 
300                 public override function remove(performAction:Function):void {
301                         performAction(new DeleteWayAction(this, setDeletedState, nodes));
302                 }
303
304                 public override function nullify():void {
305                         nullifyEntity();
306                         nodes=[];
307                         edge_l=edge_r=edge_t=edge_b=NaN;
308                 }
309                 
310                 public function get clockwise():Boolean {
311                         var lowest:uint=0;
312                         var xmin:Number=-999999; var ymin:Number=-999999;
313                         for (var i:uint=0; i<nodes.length; i++) {
314                                 if      (nodes[i].latp> ymin) { lowest=i; xmin=nodes[i].lon; ymin=nodes[i].latp; }
315                                 else if (nodes[i].latp==ymin
316                                           && nodes[i].lon > xmin) { lowest=i; xmin=nodes[i].lon; ymin=nodes[i].latp; }
317                         }
318                         return (this.onLeft(lowest)>0);
319                 }
320                 
321                 private function onLeft(j:uint):Number {
322                         var left:Number=0;
323                         var i:int, k:int;
324                         if (nodes.length>=3) {
325                                 i=j-1; if (i==-1) { i=nodes.length-2; }
326                                 k=j+1; if (k==nodes.length) { k=1; }
327                                 left=((nodes[j].lon-nodes[i].lon) * (nodes[k].latp-nodes[i].latp) -
328                                           (nodes[k].lon-nodes[i].lon) * (nodes[j].latp-nodes[i].latp));
329                         }
330                         return left;
331                 }
332
333         public function get angle():Number {
334             var dx:Number = nodes[nodes.length-1].lon - nodes[0].lon;
335             var dy:Number = nodes[nodes.length-1].latp - nodes[0].latp;
336             if (dx != 0 || dy != 0) {
337                 return Math.atan2(dx,dy)*(180/Math.PI);
338             } else {
339                 return 0;
340             }
341         }
342
343                 internal override function isEmpty():Boolean {
344                         return (deleted || (nodes.length==0));
345                 }
346
347                 public override function getType():String {
348                         return 'way';
349                 }
350                 
351                 public override function isType(str:String):Boolean {
352                         if (str=='way') return true;
353                         if (str=='line' && !isArea()) return true;
354                         if (str=='area' &&  isArea()) return true;
355                         return false;
356                 }
357                 
358                 /** Whether the way has a loop that joins back midway along its length */
359                 public function isPShape():Boolean {
360                         return getFirstNode() != getLastNode() && (!hasOnceOnly(getFirstNode()) || !hasOnceOnly(getLastNode()) );
361                 }
362                 
363                 /** Given a P-shaped way, return the index of midway node that one end connects back to. */
364                 public function getPJunctionNodeIndex():uint {
365                         if (isPShape()) {
366                             if (hasOnceOnly(getFirstNode())) {
367                                 // nodes[0] is the free end
368                                 return nodes.indexOf(getLastNode());
369                             } else {
370                                 // nodes[0] is in the loop
371                                 return nodes.lastIndexOf(getFirstNode());
372                             }
373                         }
374                         return null;
375                 }
376
377                 public function intersects(left:Number,right:Number,top:Number,bottom:Number):Boolean {
378                         // simple test first: are any nodes contained?
379                         for (var i:uint=0; i<nodes.length; i++) {
380                                 if (nodes[i].within(left,right,top,bottom)) return true;
381                         }
382                         // more complex test: do any segments cross?
383                         for (i=0; i<nodes.length-1; i++) {
384                                 if (lineIntersectsRectangle(
385                                         nodes[i  ].lon, nodes[i  ].lat,
386                                         nodes[i+1].lon, nodes[i+1].lat,
387                                         left,right,top,bottom)) return true;
388                         }
389                         return false;
390                 }
391                 
392                 private function lineIntersectsRectangle(x0:Number, y0:Number, x1:Number, y1:Number, l:Number, r:Number, b:Number, t:Number):Boolean {
393                         // from http://sebleedelisle.com/2009/05/super-fast-trianglerectangle-intersection-test/
394                         // note that t and b are transposed above because we're dealing with lat (top=90), not AS3 pixels (top=0)
395                         var m:Number = (y1-y0) / (x1-x0);
396                         var c:Number = y0 -(m*x0);
397                         var top_intersection:Number, bottom_intersection:Number;
398                         var toptrianglepoint:Number, bottomtrianglepoint:Number;
399
400                         if (m>0) {
401                                 top_intersection = (m*l  + c);
402                                 bottom_intersection = (m*r  + c);
403                         } else {
404                                 top_intersection = (m*r  + c);
405                                 bottom_intersection = (m*l  + c);
406                         }
407
408                         if (y0<y1) {
409                                 toptrianglepoint = y0;
410                                 bottomtrianglepoint = y1;
411                         } else {
412                                 toptrianglepoint = y1;
413                                 bottomtrianglepoint = y0;
414                         }
415
416                         var topoverlap:Number = top_intersection>toptrianglepoint ? top_intersection : toptrianglepoint;
417                         var botoverlap:Number = bottom_intersection<bottomtrianglepoint ? bottom_intersection : bottomtrianglepoint;
418                         return (topoverlap<botoverlap) && (!((botoverlap<t) || (topoverlap>b)));
419                 }
420
421                 public function distanceFromWay(lon:Number, latp:Number, startAt:uint=0, endAt:int=-1):Object {
422                         var i:uint, ax:Number, ay:Number, bx:Number, by:Number, l:Number;
423                         var ad:Number, bd:Number;
424                         var r:Number, d:Number, px:Number, py:Number;
425                         var furthdist:Number=-1; var furthsgn:int=1;
426                         var bestIndex:uint;
427                         if (endAt==-1) { endAt=nodes.length-1; }
428                         for (i=startAt; i<endAt; i++) {
429                                 ax=nodes[i  ].lon; ay=nodes[i  ].latp;
430                                 bx=nodes[i+1].lon; by=nodes[i+1].latp;
431
432                                 ad=Math.sqrt(Math.pow(lon-ax,2)+Math.pow(latp-ay,2));   // distance to ax,ay
433                                 bd=Math.sqrt(Math.pow(bx-lon,2)+Math.pow(by-latp,2));   // distance to bx,by
434                                 l =Math.sqrt(Math.pow(bx-ax ,2)+Math.pow(by-ay  ,2));   // length of segment
435                                 r =ad/(ad+bd);                                                                                  // proportion along segment
436                                 px=ax+r*(bx-ax); py=ay+r*(by-ay);                                               // nearest point on line
437                                 d=Math.sqrt(Math.pow(px-lon,2)+Math.pow(py-latp,2));    // distance from px,py to lon,latp
438
439                                 if (furthdist<0 || furthdist>d) {
440                                         furthdist=d;
441                                         furthsgn=sgn((bx-ax)*(latp-ay)-(by-ay)*(lon-ax));
442                                         bestIndex=i+1;
443                                 }
444                         }
445                         return { index: bestIndex, distance: furthdist*furthsgn };
446                 }
447
448                 /* This is a better algorithm than the above (doesn't screw up when near to vertices),
449                    but we need to backport the furthsgn calculation. */
450                 public function distanceFromWay2(lon:Number, latp:Number):Object {
451                         var q:Point = new Point(lon,latp);              // q is the point
452                         var dist:Number = Infinity;
453
454                         var a:Point, b:Point, daq:Point, dbq:Point, dab:Point, currentDist:Number, index:uint, u:Number;
455                         for (var i:uint=1; i<nodes.length; i++) {
456                                 a = new Point(nodes[i-1].lon, nodes[i-1].latp);
457                                 b = new Point(nodes[i  ].lon, nodes[i  ].latp);
458                                 daq = new Point(a.x-q.x, a.y-q.y);
459                                 dbq = new Point(b.x-q.x, b.y-q.y);
460                                 dab = new Point(a.x-b.x, a.y-b.y);
461                                 var inv:Number=1/(Math.pow(dab.x,2)+Math.pow(dab.y,2));
462                                 var t:Number=(dab.x*daq.x + dab.y*daq.y)*inv;
463                                 if (t>=0) {
464                                         if (t<=1) {
465                                                 currentDist = Math.pow(dab.x*dbq.y - dab.y*dbq.x, 2)*inv;
466                                         } else {
467                                                 currentDist = Math.pow(dbq.x,2)+Math.pow(dbq.y,2);
468                                         }
469                                         if (currentDist<dist) { dist=currentDist; index=i; u=t; }
470                                 }
471                         }
472                         return { index: index, distance: dist, proportion: u };
473                 }
474
475                 private function sgn(a:Number):Number {
476                         if (a==0) return 0;
477                         if (a<0) return -1;
478                         return 1;
479                 }
480     }
481 }