add rotation support and special _heading CSS variable
[potlatch2.git] / net / systemeD / halcyon / WayUI.as
1 package net.systemeD.halcyon {
2
3         import flash.display.*;
4         import flash.geom.Matrix;
5         import flash.geom.Point;
6         import flash.geom.Rectangle;
7         import flash.text.AntiAliasType;
8         import flash.text.GridFitType;
9         import flash.text.TextField;
10         import flash.text.TextFormat;
11         import flash.events.*;
12         import net.systemeD.halcyon.styleparser.*;
13     import net.systemeD.halcyon.connection.*;
14
15         public class WayUI {
16         private var way:Way;
17
18                 public var pathlength:Number;                           // length of path
19                 public var patharea:Number;                                     // area of path
20                 public var centroid_x:Number;                           // centroid
21                 public var centroid_y:Number;                           //  |
22                 public var layer:int=0;                                         // map layer
23                 public var map:Map;                                                     // reference to parent map
24                 public var sprites:Array=new Array();           // instances in display list
25                 public var heading:Array=new Array();           // angle at each node
26                 private var stateClasses:Object = new Object();
27         private var hitzone:Sprite;
28         private var listenSprite:Sprite;
29
30                 public static const DEFAULT_TEXTFIELD_PARAMS:Object = {
31                         embedFonts: true,
32                         antiAliasType: AntiAliasType.ADVANCED,
33                         gridFitType: GridFitType.NONE
34                 };
35                 public var nameformat:TextFormat;
36                 
37                 private const FILLSPRITE:uint=0;
38                 private const CASINGSPRITE:uint=1;
39                 private const STROKESPRITE:uint=2;
40                 private const NAMESPRITE:uint=3;
41                 private const NODESPRITE:uint=4;
42                 private const CLICKSPRITE:uint=5;
43
44
45                 public function WayUI(way:Way, map:Map) {
46                         this.way = way;
47                         this.map = map;
48             init();
49             way.addEventListener(Connection.TAG_CHANGE, wayTagChanged);
50             way.addEventListener(Connection.WAY_NODE_ADDED, wayNodeAdded);
51             way.addEventListener(Connection.WAY_NODE_REMOVED, wayNodeRemoved);
52             attachNodeListeners();
53                 }
54                 
55                 private function attachNodeListeners():void {
56             for (var i:uint = 0; i < way.length; i++ ) {
57                 way.getNode(i).addEventListener(Connection.NODE_MOVED, nodeMoved);
58             }
59                 }
60                 
61                 private function wayNodeAdded(event:WayNodeEvent):void {
62                     event.node.addEventListener(Connection.NODE_MOVED, nodeMoved);
63                     redraw();
64                 }
65                     
66                 private function wayNodeRemoved(event:WayNodeEvent):void {
67                     event.node.removeEventListener(Connection.NODE_MOVED, nodeMoved);
68                     redraw();
69                 }
70                     
71         private function wayTagChanged(event:TagEvent):void {
72             redraw();
73         }
74         private function nodeMoved(event:NodeMovedEvent):void {
75             redraw();
76         }
77
78                 private function init():void {
79                         recalculate();
80                         redraw();
81                         // updateBbox(lon, lat);
82                         // ** various other stuff
83                 }
84
85                 // ------------------------------------------------------------------------------------------
86                 // Calculate length etc.
87                 // ** this could be made scale-independent - would speed up redraw
88                 
89                 public function recalculate():void {
90                         var lx:Number, ly:Number, sc:Number;
91                         var node:Node, latp:Number, lon:Number;
92                         var cx:Number=0, cy:Number=0;
93                         pathlength=0;
94                         patharea=0;
95                         
96                         lx = way.getNode(way.length-1).lon;
97                         ly = way.getNode(way.length-1).latp;
98                         for ( var i:uint = 0; i < way.length; i++ ) {
99                 node = way.getNode(i);
100                 latp = node.latp;
101                 lon  = node.lon;
102
103                                 // length and area
104                                 if ( i>0 ) { pathlength += Math.sqrt( Math.pow(lon-lx,2)+Math.pow(latp-ly,2) ); }
105                                 sc = (lx*latp-lon*ly)*map.scalefactor;
106                                 cx += (lx+lon)*sc;
107                                 cy += (ly+latp)*sc;
108                                 patharea += sc;
109                                 
110                                 // heading
111                                 if (i>0) { heading[i-1]=Math.atan2((lon-lx),(latp-ly)); }
112
113                                 lx=lon; ly=latp;
114                         }
115                         heading[way.length-1]=heading[way.length-2];
116
117                         pathlength*=map.scalefactor;
118                         patharea/=2;
119                         if (patharea!=0 && way.isArea()) {
120                                 centroid_x=map.lon2coord(cx/patharea/6);
121                                 centroid_y=map.latp2coord(cy/patharea/6);
122                         } else if (pathlength>0) {
123                                 var c:Array=pointAt(0.5);
124                                 centroid_x=c[0];
125                                 centroid_y=c[1];
126                         }
127                 }
128
129                 // ------------------------------------------------------------------------------------------
130                 // Redraw
131
132                 public function redraw():void {
133             // Copy tags object, and add states
134             var tags:Object = way.getTagsCopy();
135             for (var stateKey:String in stateClasses) {
136                 tags[":"+stateKey] = stateKey;
137             }
138                         if (way.isArea()) { tags[':area']='yes'; }
139
140                         // Remove all currently existing sprites
141                         while (sprites.length>0) {
142                                 var d:DisplayObject=sprites.pop(); d.parent.removeChild(d);
143                         }
144
145                         // Which layer?
146                         layer=5;
147                         if ( tags['layer'] )
148                 layer=Math.min(Math.max(tags['layer']+5,-5),5)+5;
149
150                         // Iterate through each sublayer, drawing any styles on that layer
151                         var sl:StyleList=map.ruleset.getStyles(this.way, tags);
152                         var drawn:Boolean;
153                         for (var sublayer:uint=0; sublayer<11; sublayer++) {
154                                 if (sl.shapeStyles[sublayer]) {
155                                         var s:ShapeStyle=sl.shapeStyles[sublayer];
156                                         var stroke:Shape, fill:Shape, casing:Shape, roadname:Sprite;
157                                         var x0:Number=map.lon2coord(way.getNode(0).lon);
158                                         var y0:Number=map.latp2coord(way.getNode(0).latp);
159
160                                         // Stroke
161                                         if (s.width)  {
162                                                 stroke=new Shape(); addToLayer(stroke,STROKESPRITE,sublayer);
163                                                 stroke.graphics.moveTo(x0,y0);
164                                                 s.applyStrokeStyle(stroke.graphics);
165                                                 if (s.dashes && s.dashes.length>0) { dashedLine(stroke.graphics,s.dashes); }
166                                                                                                           else { solidLine(stroke.graphics); }
167                                                 drawn=true;
168                                         }
169
170                                         // Fill
171                                         if (s.fill_color || s.fill_image) {
172                                                 fill=new Shape(); addToLayer(fill,FILLSPRITE);
173                                                 fill.graphics.moveTo(x0,y0);
174                                                 if (s.fill_image) { new WayBitmapFiller(this,fill.graphics,s); }
175                                                                          else { s.applyFill(fill.graphics); }
176                                                 solidLine(fill.graphics);
177                                                 fill.graphics.endFill();
178                                                 drawn=true;
179                                         }
180
181                                         // Casing
182                                         if (s.casing_width) { 
183                                                 casing=new Shape(); addToLayer(casing,CASINGSPRITE);
184                                                 casing.graphics.moveTo(x0,y0);
185                                                 s.applyCasingStyle(casing.graphics);
186                                                 if (s.casing_dashes && s.casing_dashes.length>0) { dashedLine(casing.graphics,s.casing_dashes); }
187                                                                                                                                         else { solidLine(casing.graphics); }
188                                                 drawn=true;
189                                         }
190                                 }
191                                 
192                                 if (sl.textStyles[sublayer]) {
193                                         var t:TextStyle=sl.textStyles[sublayer];
194                                         roadname=new Sprite(); addToLayer(roadname,NAMESPRITE);
195                                         nameformat = t.getTextFormat();
196                                         var a:String=tags[t.text];
197                                         if (a) {
198                                                 if (t.font_caps) { a=a.toUpperCase(); }
199                                                 if (t.text_center && centroid_x) {
200                                                         t.writeNameLabel(roadname,a,centroid_x,centroid_y);
201                                                 } else {
202                                                         writeNameOnPath(roadname,a,t.text_offset ? t.text_offset : 0);
203                                                 }
204                                                 if (t.text_halo_radius>0) { roadname.filters=t.getHaloFilter(); }
205                                         }
206                                 }
207                                 
208                                 // ** ShieldStyle to do
209                         }
210
211                         // ** draw icons
212                         var r:Number;
213                         for (var i:uint = 0; i < way.length; i++) {
214                 var node:Node = way.getNode(i);
215                     if (map.pois[node.id]) {
216                                         if (map.pois[node.id].loaded) {
217                                                 map.pois[node.id].redraw();
218                                         }
219                                 } else if (node.hasTags()) {
220                                         sl=map.ruleset.getStyles(node,node.getTagsHash());
221                                         if (sl.hasStyles()) {
222                                                 if (i==0) { r= heading[i]; }
223                                                      else { r=(heading[i]+heading[i-1])/2; }
224                                                 map.pois[node.id]=new POI(node,map,sl,r);
225                                                 // ** this should be done via the registerPOI/event listener mechanism,
226                                                 //    but that needs a bit of reworking so we can pass in a styleList
227                                                 //    (otherwise we end up computing the styles twice which is expensive)
228                                         }
229                                 }
230                         }
231                         
232                         
233
234                         // No styles, so add a thin trace
235             if (!drawn && map.showall) {
236                 var def:Sprite = new Sprite();
237                 def.graphics.lineStyle(0.5, 0x808080, 1, false, "normal");
238                 solidLine(def.graphics);
239                 addToLayer(def, STROKESPRITE);          // ** this probably needs a sublayer
240                                 drawn=true;
241             }
242             
243             if ( stateClasses["showNodes"] != null ) {
244                 var nodes:Sprite = new Sprite();
245                 drawNodes(nodes.graphics);
246                 addToLayer(nodes, NODESPRITE);
247             }
248
249                         if (!drawn) { return; }
250                         
251             // create a generic "way" hitzone sprite
252             hitzone = new Sprite();
253             hitzone.graphics.lineStyle(4, 0x000000, 1, false, "normal", CapsStyle.ROUND, JointStyle.ROUND);
254             solidLine(hitzone.graphics);
255             addToLayer(hitzone, CLICKSPRITE);
256             hitzone.visible = false;
257
258             if ( listenSprite == null ) {
259                 listenSprite = new Sprite();
260                 listenSprite.addEventListener(MouseEvent.CLICK, mouseEvent);
261                 listenSprite.addEventListener(MouseEvent.DOUBLE_CLICK, mouseEvent);
262                 listenSprite.addEventListener(MouseEvent.MOUSE_OVER, mouseEvent);
263                 listenSprite.addEventListener(MouseEvent.MOUSE_OUT, mouseEvent);
264                 listenSprite.addEventListener(MouseEvent.MOUSE_DOWN, mouseEvent);
265                 listenSprite.addEventListener(MouseEvent.MOUSE_UP, mouseEvent);
266                 listenSprite.addEventListener(MouseEvent.MOUSE_MOVE, mouseEvent);
267             }
268             listenSprite.hitArea = hitzone;
269             addToLayer(listenSprite, CLICKSPRITE);
270             listenSprite.buttonMode = true;
271             listenSprite.mouseEnabled = true;
272
273                 }
274                 
275                 // ------------------------------------------------------------------------------------------
276                 // Drawing support functions
277
278                 private function drawNodes(g:Graphics):void {
279             g.lineStyle(1, 0xff0000, 1, false, "normal", CapsStyle.ROUND, JointStyle.ROUND);
280                         for (var i:uint = 0; i < way.length; i++) {
281                 var node:Node = way.getNode(i);
282                 var x:Number = map.lon2coord(node.lon);
283                 var y:Number = map.latp2coord(node.latp);
284                 g.moveTo(x-2, y-2);
285                 g.lineTo(x+2, y-2);
286                 g.lineTo(x+2, y+2);
287                 g.lineTo(x-2, y+2);
288                 g.lineTo(x-2, y-2);
289                         }
290                 }
291
292                 // Draw solid polyline
293                 
294                 public function solidLine(g:Graphics):void {
295             var node:Node = way.getNode(0);
296                         g.moveTo(map.lon2coord(node.lon), map.latp2coord(node.latp));
297                         for (var i:uint = 1; i < way.length; i++) {
298                 node = way.getNode(i);
299                                 g.lineTo(map.lon2coord(node.lon), map.latp2coord(node.latp));
300                         }
301                 }
302
303                 // Draw dashed polyline
304                 
305                 private function dashedLine(g:Graphics,dashes:Array):void {
306                         var draw:Boolean=false, dashleft:Number=0, dc:Array=new Array();
307                         var a:Number, xc:Number, yc:Number;
308                         var curx:Number, cury:Number;
309                         var dx:Number, dy:Number, segleft:Number=0;
310                         var i:int=0;
311
312             var node:Node = way.getNode(0);
313             var nextNode:Node = way.getNode(0);
314                         g.moveTo(map.lon2coord(node.lon), map.latp2coord(node.latp));
315                         while (i < way.length-1 || segleft>0) {
316                                 if (dashleft<=0) {      // should be ==0
317                                         if (dc.length==0) { dc=dashes.slice(0); }
318                                         dashleft=dc.shift();
319                                         draw=!draw;
320                                 }
321                                 if (segleft<=0) {       // should be ==0
322                     node = way.getNode(i);
323                     nextNode = way.getNode(i+1);
324                                         curx=map.lon2coord(node.lon);
325                     dx=map.lon2coord(nextNode.lon)-curx;
326                                         cury=map.latp2coord(node.latp);
327                     dy=map.latp2coord(nextNode.latp)-cury;
328                                         a=Math.atan2(dy,dx); xc=Math.cos(a); yc=Math.sin(a);
329                                         segleft=Math.sqrt(dx*dx+dy*dy);
330                                         i++;
331                                 }
332
333                                 if (segleft<=dashleft) {
334                                         // the path segment is shorter than the dash
335                                         curx+=dx; cury+=dy;
336                                         moveLine(g,curx,cury,draw);
337                                         dashleft-=segleft; segleft=0;
338                                 } else {
339                                         // the path segment is longer than the dash
340                                         curx+=dashleft*xc; dx-=dashleft*xc;
341                                         cury+=dashleft*yc; dy-=dashleft*yc;
342                                         moveLine(g,curx,cury,draw);
343                                         segleft-=dashleft; dashleft=0;
344                                 }
345                         }
346                 }
347
348                 private function moveLine(g:Graphics,x:Number,y:Number,draw:Boolean):void {
349                         if (draw) { g.lineTo(x,y); }
350                                  else { g.moveTo(x,y); }
351                 }
352
353                 
354                 // Find point partway (0-1) along a path
355                 // returns (x,y,angle)
356                 // inspired by senocular's Path.as
357                 
358                 private function pointAt(t:Number):Array {
359                         var totallen:Number = t*pathlength;
360                         var curlen:Number = 0;
361                         var dx:Number, dy:Number, seglen:Number;
362                         for (var i:int = 1; i < way.length; i++){
363                                 dx=map.lon2coord(way.getNode(i).lon)-map.lon2coord(way.getNode(i-1).lon);
364                                 dy=map.latp2coord(way.getNode(i).latp)-map.latp2coord(way.getNode(i-1).latp);
365                                 seglen=Math.sqrt(dx*dx+dy*dy);
366                                 if (totallen > curlen+seglen) { curlen+=seglen; continue; }
367                                 return new Array(map.lon2coord(way.getNode(i-1).lon)+(totallen-curlen)/seglen*dx,
368                                                                  map.latp2coord(way.getNode(i-1).latp)+(totallen-curlen)/seglen*dy,
369                                                                  Math.atan2(dy,dx));
370                         }
371                         return new Array(0, 0, 0);
372                 }
373
374                 // Draw name along path
375                 // based on code by Tom Carden
376                 
377                 private function writeNameOnPath(s:Sprite,a:String,textOffset:Number=0):void {
378
379                         // make a dummy textfield so we can measure its width
380                         var tf:TextField = new TextField();
381                         tf.defaultTextFormat = nameformat;
382                         tf.text = a;
383                         tf.width = tf.textWidth+4;
384                         tf.height = tf.textHeight+4;
385                         if (pathlength<tf.width) { return; }    // no room for text?
386
387                         var t1:Number = (pathlength/2 - tf.width/2) / pathlength; var p1:Array=pointAt(t1);
388                         var t2:Number = (pathlength/2 + tf.width/2) / pathlength; var p2:Array=pointAt(t2);
389
390                         var angleOffset:Number; // so we can do a 180ยบ if we're running backwards
391                         var offsetSign:Number;  // -1 if we're starting at t2
392                         var tStart:Number;      // t1 or t2
393
394                         // make sure text doesn't run right->left or upside down
395                         if (p1[0] < p2[0] && 
396                                 p1[2] < Math.PI/2 &&
397                                 p1[2] > -Math.PI/2) {
398                                 angleOffset = 0; offsetSign = 1; tStart = t1;
399                         } else {
400                                 angleOffset = Math.PI; offsetSign = -1; tStart = t2;
401                         } 
402
403                         // make a textfield for each char, centered on the line,
404                         // using getCharBoundaries to rotate it around its center point
405                         var chars:Array = a.split('');
406                         for (var i:int = 0; i < chars.length; i++) {
407                                 var rect:Rectangle = tf.getCharBoundaries(i);
408                                 if (rect) {
409                                         s.addChild(rotatedLetter(chars[i],
410                                                                                          tStart + offsetSign*(rect.left+rect.width/2)/pathlength,
411                                                                                          rect.width, tf.height, angleOffset, textOffset));
412                                 }
413                         }
414                 }
415
416                 private function rotatedLetter(char:String, t:Number, w:Number, h:Number, a:Number, o:Number):TextField {
417                         var tf:TextField = new TextField();
418             tf.mouseEnabled = false;
419             tf.mouseWheelEnabled = false;
420                         tf.defaultTextFormat = nameformat;
421                         tf.embedFonts = true;
422                         tf.text = char;
423                         tf.width = tf.textWidth+4;
424                         tf.height = tf.textHeight+4;
425
426                         var p:Array=pointAt(t);
427                         var matrix:Matrix = new Matrix();
428                         matrix.translate(-w/2, -h/2-o);
429                         // ** add (say) -4 to the height to move it up by 4px
430                         matrix.rotate(p[2]+a);
431                         matrix.translate(p[0], p[1]);
432                         tf.transform.matrix = matrix;
433                         return tf;
434                 }
435                 
436                 // Add object (stroke/fill/roadname) to layer sprite
437                 
438                 private function addToLayer(s:DisplayObject,t:uint,sublayer:int=-1):void {
439                         var l:DisplayObject=Map(map).getChildAt(map.WAYSPRITE+layer);
440                         var o:DisplayObject=Sprite(l).getChildAt(t);
441                         if (sublayer!=-1) { o=Sprite(o).getChildAt(sublayer); }
442                         Sprite(o).addChild(s);
443                         sprites.push(s);
444             if ( s is Sprite ) {
445                 Sprite(s).mouseEnabled = false;
446                 Sprite(s).mouseChildren = false;
447             }
448                 }
449
450                 public function getNodeAt(x:Number, y:Number):Node {
451                         for (var i:uint = 0; i < way.length; i++) {
452                 var node:Node = way.getNode(i);
453                 var nodeX:Number = map.lon2coord(node.lon);
454                 var nodeY:Number = map.latp2coord(node.latp);
455                 if ( nodeX >= x-3 && nodeX <= x+3 &&
456                      nodeY >= y-3 && nodeY <= y+3 )
457                     return node;
458             }
459             return null;
460                 }
461
462         private function mouseEvent(event:MouseEvent):void {
463             var node:Node = getNodeAt(event.localX, event.localY);
464             if ( node == null )
465                 map.entityMouseEvent(event, way);
466             else
467                 map.entityMouseEvent(event, node);
468         }
469
470         public function setHighlight(stateType:String, isOn:Boolean):void {
471             if ( isOn && stateClasses[stateType] == null ) {
472                 stateClasses[stateType] = true;
473                 redraw();
474             } else if ( !isOn && stateClasses[stateType] != null ) {
475                 delete stateClasses[stateType];
476                 redraw();
477             }
478         }
479         }
480 }