Add sublayers (stacking order) for stroke drawing
[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 net.systemeD.halcyon.styleparser.*;
12     import net.systemeD.halcyon.connection.*;
13
14         public class WayUI {
15         private var way:Way;
16
17                 public var pathlength:Number;                           // length of path
18
19                 public var layer:int=0;                                         // map layer
20                 public var map:Map;                                                     // reference to parent map
21                 public var stroke:Sprite;                                       // instance in display list
22                 public var fill:Sprite;                                         //  |
23                 public var roadname:Sprite;                                     //  |
24
25                 public static const DEFAULT_TEXTFIELD_PARAMS:Object = {
26                         embedFonts: true,
27                         antiAliasType: AntiAliasType.ADVANCED,
28                         gridFitType: GridFitType.NONE
29                 };
30                 [Embed(source="fonts/DejaVuSans.ttf", fontFamily="DejaVu", fontWeight="normal", mimeType="application/x-font-truetype")]
31                 public static var DejaVu:Class;
32                 public var nameformat:TextFormat;
33
34
35                 public function WayUI(way:Way, map:Map) {
36                         this.way = way;
37                         this.map = map;
38             init();
39                 }
40                 
41                 private function init():void {
42                         var lx:Number, ly:Number;
43                         pathlength=0;
44                         
45                         for ( var i:uint = 0; i < way.length; i++ ) {
46                 var node:Node = way.getNode(i);
47                 var lat:Number = node.lat;
48                 var lon:Number = node.lon;
49 //                              updateBbox(lon, lat);
50                                 if ( !isNaN(lx) ) {
51                     pathlength += Math.sqrt( Math.pow(lon-lx,2)+Math.pow(lat-ly,2) );
52                 }
53                                 lx=lon; ly=lat;
54                         }
55
56                         pathlength*=map.scalefactor;
57                         redraw();
58                         // ** various other stuff
59                 }
60
61                 // ------------------------------------------------------------------------------------------
62                 // Redraw
63
64                 public function redraw():void {
65             var tags:Object = way.getTagsCopy();
66
67                         // ** remove previous version from any layer/sublayer
68                         layer=5;
69                         if ( tags['layer'] )
70                 layer=Math.min(Math.max(tags['layer']+5,-5),5)+5;
71
72                         // set style
73                         var styles:Array=map.ruleset.getStyle(false, tags, map.scale);
74                         var sublayer:uint=0; if (styles[0] && styles[0].sublayer) { sublayer=styles[0].sublayer; }
75
76                         // find/create sprites
77                         if (stroke) {
78                                 fill.graphics.clear(); 
79                                 stroke.graphics.clear(); 
80                                 roadname.graphics.clear();
81                                 while (roadname.numChildren) { roadname.removeChildAt(0); }
82                         } else {
83                                 fill=new Sprite(); addToLayer(fill,0);
84                                 stroke=new Sprite(); addToLayer(stroke,1,sublayer); 
85                                 roadname=new Sprite(); addToLayer(roadname,2); 
86                         }
87                         var g:Graphics=stroke.graphics;
88                         var f:Graphics=fill.graphics;
89
90                         // ShapeStyle
91                         // ** do line-caps/joints
92                         var doStroke:Boolean=false, doDashed:Boolean=false;
93                         var doFill:Boolean=false, fill_colour:uint, fill_opacity:Number;
94                         var doCasing:Boolean=false, doDashedCasing:Boolean=false;
95                         if (styles[0]) {
96                                 var ss:ShapeStyle=styles[0];
97                                 if (ss.isStroked) {     doStroke=true;
98                                                                         doDashed=(ss.stroke_dashArray.length>0);
99                                                                         g.lineStyle(ss.stroke_width, ss.stroke_colour, ss.stroke_opacity/100,
100                                                                                                 false,"normal", ss.stroke_linecap,ss.stroke_linejoin); }
101                                 if (ss.isCased)   { doCasing=true;
102                                                                         doDashedCasing=(ss.casing_dashArray.length>0);
103                                                                         f.lineStyle(ss.casing_width, ss.casing_colour, ss.casing_opacity/100,
104                                                                                                 false,"normal", ss.stroke_linecap, ss.stroke_linejoin); }
105                                 if (ss.isFilled)  { doFill=true;
106                                                                         fill_colour = ss.fill_colour;
107                                                                         fill_opacity= ss.fill_opacity/100; }
108                         }
109
110                         // draw line
111                         if (doFill)
112                 f.beginFill(fill_colour,fill_opacity);
113                         if (doStroke)
114                 g.moveTo(map.lon2coord(way.getNode(0).lon), map.latp2coord(way.getNode(0).latp));
115                         if (doFill || doCasing)
116                 f.moveTo(map.lon2coord(way.getNode(0).lon), map.latp2coord(way.getNode(0).latp));
117
118                         if (doDashed)
119                 dashedLine(g,ss.stroke_dashArray);
120                         else if (doStroke)
121                 solidLine(g);
122                         
123                         if (doDashedCasing) {
124                 dashedLine(f,ss.casing_dashArray);
125                 f.lineStyle();
126             }
127                         if (doFill) {
128                                 f.beginFill(fill_colour,fill_opacity); 
129                                 solidLine(f);
130                                 f.endFill(); 
131                         } else if (doCasing && !doDashedCasing) {
132                 solidLine(f);
133             }
134
135                         // TextStyle
136                         // ** do pull-out
137                         if (styles[2] && styles[2].tag && tags[styles[2].tag]) {
138                                 var ts:TextStyle=styles[2];
139                                 nameformat = new TextFormat(ts.font_name   ? ts.font_name : "DejaVu",
140                                                                                         ts.text_size   ? ts.text_size : 8,
141                                                                                         ts.text_colour ? ts.text_colour: 0,
142                                                                                         ts.font_bold   ? ts.font_bold : false,
143                                                                                         ts.font_italic ? ts.font_italic: false);
144                                 var a:String=tags[ts.tag]; if (ts.font_caps) { a=a.toUpperCase(); }
145                                 writeName(roadname,a,ts.text_offset ? ts.text_offset : 0);
146                         }
147                         // ShieldStyle - 3
148                         // ** to do
149                 }
150                 
151                 // ------------------------------------------------------------------------------------------
152                 // Drawing support functions
153
154                 // Draw solid polyline
155                 
156                 private function solidLine(g:Graphics):void {
157             var node:Node = way.getNode(0);
158                         g.moveTo(map.lon2coord(node.lon), map.latp2coord(node.latp));
159                         for (var i:uint = 1; i < way.length; i++) {
160                 node = way.getNode(i);
161                                 g.lineTo(map.lon2coord(node.lon), map.latp2coord(node.latp));
162                         }
163                 }
164
165                 // Draw dashed polyline
166                 
167                 private function dashedLine(g:Graphics,dashes:Array):void {
168                         var draw:Boolean=false, dashleft:Number=0, dc:Array=new Array();
169                         var a:Number, xc:Number, yc:Number;
170                         var curx:Number, cury:Number;
171                         var dx:Number, dy:Number, segleft:Number=0;
172                         var i:int=0;
173
174             var node:Node = way.getNode(0);
175             var nextNode:Node = way.getNode(0);
176                         g.moveTo(map.lon2coord(node.lon), map.latp2coord(node.latp));
177                         while (i < way.length-1 || segleft>0) {
178                                 if (dashleft<=0) {      // should be ==0
179                                         if (dc.length==0) { dc=dashes.slice(0); }
180                                         dashleft=dc.shift();
181                                         draw=!draw;
182                                 }
183                                 if (segleft<=0) {       // should be ==0
184                     node = way.getNode(i);
185                     nextNode = way.getNode(i+1);
186                                         curx=map.lon2coord(node.lon);
187                     dx=map.lon2coord(nextNode.lon)-curx;
188                                         cury=map.latp2coord(node.latp);
189                     dy=map.latp2coord(nextNode.latp)-cury;
190                                         a=Math.atan2(dy,dx); xc=Math.cos(a); yc=Math.sin(a);
191                                         segleft=Math.sqrt(dx*dx+dy*dy);
192                                         i++;
193                                 }
194
195                                 if (segleft<=dashleft) {
196                                         // the path segment is shorter than the dash
197                                         curx+=dx; cury+=dy;
198                                         moveLine(g,curx,cury,draw);
199                                         dashleft-=segleft; segleft=0;
200                                 } else {
201                                         // the path segment is longer than the dash
202                                         curx+=dashleft*xc; dx-=dashleft*xc;
203                                         cury+=dashleft*yc; dy-=dashleft*yc;
204                                         moveLine(g,curx,cury,draw);
205                                         segleft-=dashleft; dashleft=0;
206                                 }
207                         }
208                 }
209
210                 private function moveLine(g:Graphics,x:Number,y:Number,draw:Boolean):void {
211                         if (draw) { g.lineTo(x,y); }
212                                  else { g.moveTo(x,y); }
213                 }
214
215                 
216                 // Find point partway (0-1) along a path
217                 // returns (x,y,angle)
218                 // inspired by senocular's Path.as
219                 
220                 private function pointAt(t:Number):Array {
221                         var totallen:Number = t*pathlength;
222                         var curlen:Number = 0;
223                         var dx:Number, dy:Number, seglen:Number;
224                         for (var i:int = 1; i < way.length; i++){
225                                 dx=map.lon2coord(way.getNode(i).lon)-map.lon2coord(way.getNode(i-1).lon);
226                                 dy=map.latp2coord(way.getNode(i).latp)-map.latp2coord(way.getNode(i-1).latp);
227                                 seglen=Math.sqrt(dx*dx+dy*dy);
228                                 if (totallen > curlen+seglen) { curlen+=seglen; continue; }
229                                 return new Array(map.lon2coord(way.getNode(i-1).lon)+(totallen-curlen)/seglen*dx,
230                                                                  map.latp2coord(way.getNode(i-1).latp)+(totallen-curlen)/seglen*dy,
231                                                                  Math.atan2(dy,dx));
232                         }
233                         return new Array(0, 0, 0);
234                 }
235
236                 // Draw name along path
237                 // based on code by Tom Carden
238                 // ** needs styling
239                 
240                 private function writeName(s:Sprite,a:String,textOffset:Number=0):void {
241
242                         // make a dummy textfield so we can measure its width
243                         var tf:TextField = new TextField();
244                         tf.defaultTextFormat = nameformat;
245                         tf.text = a;
246                         tf.width = tf.textWidth+4;
247                         tf.height = tf.textHeight+4;
248                         if (pathlength<tf.width) { return; }    // no room for text?
249
250                         var t1:Number = (pathlength/2 - tf.width/2) / pathlength; var p1:Array=pointAt(t1);
251                         var t2:Number = (pathlength/2 + tf.width/2) / pathlength; var p2:Array=pointAt(t2);
252
253                         var angleOffset:Number; // so we can do a 180ยบ if we're running backwards
254                         var offsetSign:Number;  // -1 if we're starting at t2
255                         var tStart:Number;      // t1 or t2
256
257                         // make sure text doesn't run right->left or upside down
258                         if (p1[0] < p2[0] && 
259                                 p1[2] < Math.PI/2 &&
260                                 p1[2] > -Math.PI/2) {
261                                 angleOffset = 0; offsetSign = 1; tStart = t1;
262                         } else {
263                                 angleOffset = Math.PI; offsetSign = -1; tStart = t2;
264                         } 
265
266                         // make a textfield for each char, centered on the line,
267                         // using getCharBoundaries to rotate it around its center point
268                         var chars:Array = a.split('');
269                         for (var i:int = 0; i < chars.length; i++) {
270                                 var rect:Rectangle = tf.getCharBoundaries(i);
271                                 if (rect) {
272                                         s.addChild(rotatedLetter(chars[i],
273                                                                                          tStart + offsetSign*(rect.left+rect.width/2)/pathlength,
274                                                                                          rect.width, tf.height, angleOffset, textOffset));
275                                 }
276                         }
277                 }
278
279                 private function rotatedLetter(char:String, t:Number, w:Number, h:Number, a:Number, o:Number):TextField {
280                         var tf:TextField = new TextField();
281                         tf.embedFonts = true;
282                         tf.defaultTextFormat = nameformat;
283                         tf.text = char;
284                         tf.width = tf.textWidth+4;
285                         tf.height = tf.textHeight+4;
286
287                         var p:Array=pointAt(t);
288                         var matrix:Matrix = new Matrix();
289                         matrix.translate(-w/2, -h/2-o);
290                         // ** add (say) -4 to the height to move it up by 4px
291                         matrix.rotate(p[2]+a);
292                         matrix.translate(p[0], p[1]);
293                         tf.transform.matrix = matrix;
294                         return tf;
295                 }
296                 
297                 // Add object (stroke/fill/roadname) to layer sprite
298                 
299                 private function addToLayer(s:Sprite,t:uint,sublayer:int=-1):void {
300                         var l:DisplayObject=Map(map).getChildAt(layer);
301                         var o:DisplayObject=Sprite(l).getChildAt(t);
302                         if (sublayer!=-1) { o=Sprite(o).getChildAt(sublayer); }
303                         Sprite(o).addChild(s);
304                 }
305         }
306 }