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