1 package net.systemeD.halcyon {
3 import flash.display.*;
4 import flash.geom.Matrix;
5 import flash.geom.Point;
6 import flash.geom.Rectangle;
7 import flash.text.TextField;
8 import flash.text.TextFormat;
10 import net.systemeD.halcyon.styleparser.*;
11 import net.systemeD.halcyon.connection.*;
12 import net.systemeD.halcyon.Globals;
14 public class WayUI extends EntityUI {
17 public var pathlength:Number; // length of path
18 public var patharea:Number; // area of path
19 public var centroid_x:Number; // centroid
20 public var centroid_y:Number; // |
21 public var heading:Array=new Array(); // angle at each node
22 private var hitzone:Sprite;
23 public var nameformat:TextFormat;
25 private const NODESIZE:uint=6;
27 public function WayUI(way:Way, map:Map) {
32 way.addEventListener(Connection.TAG_CHANGE, wayTagChanged);
33 way.addEventListener(Connection.WAY_NODE_ADDED, wayNodeAdded);
34 way.addEventListener(Connection.WAY_NODE_REMOVED, wayNodeRemoved);
35 attachNodeListeners();
38 private function attachNodeListeners():void {
39 for (var i:uint = 0; i < way.length; i++ ) {
40 way.getNode(i).addEventListener(Connection.NODE_MOVED, nodeMoved);
44 private function wayNodeAdded(event:WayNodeEvent):void {
45 event.node.addEventListener(Connection.NODE_MOVED, nodeMoved);
49 private function wayNodeRemoved(event:WayNodeEvent):void {
50 event.node.removeEventListener(Connection.NODE_MOVED, nodeMoved);
54 private function wayTagChanged(event:TagEvent):void {
57 private function nodeMoved(event:NodeMovedEvent):void {
61 private function init():void {
64 // updateBbox(lon, lat);
65 // ** various other stuff
68 // ------------------------------------------------------------------------------------------
69 // Calculate length etc.
70 // ** this could be made scale-independent - would speed up redraw
72 public function recalculate():void {
73 var lx:Number, ly:Number, sc:Number;
74 var node:Node, latp:Number, lon:Number;
75 var cx:Number=0, cy:Number=0;
79 lx = way.getNode(way.length-1).lon;
80 ly = way.getNode(way.length-1).latp;
81 for ( var i:uint = 0; i < way.length; i++ ) {
82 node = way.getNode(i);
87 if ( i>0 ) { pathlength += Math.sqrt( Math.pow(lon-lx,2)+Math.pow(latp-ly,2) ); }
88 sc = (lx*latp-lon*ly)*map.scalefactor;
94 if (i>0) { heading[i-1]=Math.atan2((lon-lx),(latp-ly)); }
98 heading[way.length-1]=heading[way.length-2];
100 pathlength*=map.scalefactor;
102 if (patharea!=0 && way.isArea()) {
103 centroid_x=map.lon2coord(cx/patharea/6);
104 centroid_y=map.latp2coord(cy/patharea/6);
105 } else if (pathlength>0) {
106 var c:Array=pointAt(0.5);
112 // ------------------------------------------------------------------------------------------
115 public function redraw():void {
118 // Copy tags object, and add states
119 var tags:Object = way.getTagsCopy();
120 for (var stateKey:String in stateClasses) {
121 tags[":"+stateKey] = 'yes';
123 if (way.isArea()) { tags[':area']='yes'; }
128 layer=Math.min(Math.max(tags['layer']+5,-5),5)+5;
130 // Iterate through each sublayer, drawing any styles on that layer
131 var sl:StyleList=map.ruleset.getStyles(this.way, tags);
133 for (var sublayer:int=10; sublayer>=0; sublayer--) {
134 if (sl.shapeStyles[sublayer]) {
135 var s:ShapeStyle=sl.shapeStyles[sublayer];
136 var stroke:Shape, fill:Shape, casing:Shape, roadname:Sprite;
137 var x0:Number=map.lon2coord(way.getNode(0).lon);
138 var y0:Number=map.latp2coord(way.getNode(0).latp);
142 stroke=new Shape(); addToLayer(stroke,STROKESPRITE,sublayer);
143 stroke.graphics.moveTo(x0,y0);
144 s.applyStrokeStyle(stroke.graphics);
145 if (s.dashes && s.dashes.length>0) { dashedLine(stroke.graphics,s.dashes); }
146 else { solidLine(stroke.graphics); }
151 if (s.fill_color || s.fill_image) {
152 fill=new Shape(); addToLayer(fill,FILLSPRITE);
153 fill.graphics.moveTo(x0,y0);
154 if (s.fill_image) { new WayBitmapFiller(this,fill.graphics,s); }
155 else { s.applyFill(fill.graphics); }
156 solidLine(fill.graphics);
157 fill.graphics.endFill();
162 if (s.casing_width) {
163 casing=new Shape(); addToLayer(casing,CASINGSPRITE);
164 casing.graphics.moveTo(x0,y0);
165 s.applyCasingStyle(casing.graphics);
166 if (s.casing_dashes && s.casing_dashes.length>0) { dashedLine(casing.graphics,s.casing_dashes); }
167 else { solidLine(casing.graphics); }
172 if (sl.textStyles[sublayer]) {
173 var t:TextStyle=sl.textStyles[sublayer];
174 roadname=new Sprite(); addToLayer(roadname,NAMESPRITE);
175 nameformat = t.getTextFormat();
176 var a:String=tags[t.text];
178 if (t.font_caps) { a=a.toUpperCase(); }
179 if (t.text_center && centroid_x) {
180 t.writeNameLabel(roadname,a,centroid_x,centroid_y);
182 writeNameOnPath(roadname,a,t.text_offset ? t.text_offset : 0);
184 if (t.text_halo_radius>0) { roadname.filters=t.getHaloFilter(); }
188 // ** ShieldStyle to do
192 // ** there should be huge potential to optimise this - at present we're
193 // running getStyles for every node in the way on every redraw
196 var highlight:Boolean=stateClasses["showNodes"]; // !=null
197 for (var i:uint = 0; i < way.length; i++) {
198 var node:Node = way.getNode(i);
199 nodetags=node.getTagsCopy();
200 if (i==0) { nodetags['_heading']= heading[i]; }
201 else { nodetags['_heading']=(heading[i]+heading[i-1])/2; }
202 if (highlight) { nodetags[':selectedway']='yes'; }
203 sl=map.ruleset.getStyles(node,nodetags);
204 if (sl.hasStyles()) {
205 if (!map.pois[node.id]) { map.pois[node.id]=new NodeUI(node,map,r); }
206 map.pois[node.id].redraw(sl);
207 // ** this should be done via the registerPOI/event listener mechanism,
208 // but that needs a bit of reworking so we can pass in a styleList
209 // (otherwise we end up computing the styles twice which is expensive)
210 } else if (map.pois[node.id]) {
211 map.pois[node.id].removeSprites();
214 if (!drawn) { return; }
216 // create a generic "way" hitzone sprite
217 hitzone = new Sprite();
218 hitzone.graphics.lineStyle(4, 0x000000, 1, false, "normal", CapsStyle.ROUND, JointStyle.ROUND);
219 solidLine(hitzone.graphics);
220 addToLayer(hitzone, CLICKSPRITE);
221 hitzone.visible = false;
222 createListenSprite(hitzone);
226 // ------------------------------------------------------------------------------------------
227 // Drawing support functions
229 private function drawNodes(g:Graphics):void {
230 // ***** these should be discreet anchorpoints (NodeUI?), not just sprites
231 // g.lineStyle(1, 0xff0000, 1, false, "normal", CapsStyle.ROUND, JointStyle.ROUND);
232 g.beginFill(0xFF0000);
233 for (var i:uint = 0; i < way.length; i++) {
234 var node:Node = way.getNode(i);
235 var x:Number = map.lon2coord(node.lon);
236 var y:Number = map.latp2coord(node.latp);
237 g.moveTo(x-NODESIZE, y-NODESIZE);
238 g.lineTo(x+NODESIZE, y-NODESIZE);
239 g.lineTo(x+NODESIZE, y+NODESIZE);
240 g.lineTo(x-NODESIZE, y+NODESIZE);
241 g.lineTo(x-NODESIZE, y-NODESIZE);
245 // Draw solid polyline
247 public function solidLine(g:Graphics):void {
248 var node:Node = way.getNode(0);
249 g.moveTo(map.lon2coord(node.lon), map.latp2coord(node.latp));
250 for (var i:uint = 1; i < way.length; i++) {
251 node = way.getNode(i);
252 g.lineTo(map.lon2coord(node.lon), map.latp2coord(node.latp));
256 // Draw dashed polyline
258 private function dashedLine(g:Graphics,dashes:Array):void {
259 var draw:Boolean=false, dashleft:Number=0, dc:Array=new Array();
260 var a:Number, xc:Number, yc:Number;
261 var curx:Number, cury:Number;
262 var dx:Number, dy:Number, segleft:Number=0;
265 var node:Node = way.getNode(0);
266 var nextNode:Node = way.getNode(0);
267 g.moveTo(map.lon2coord(node.lon), map.latp2coord(node.latp));
268 while (i < way.length-1 || segleft>0) {
269 if (dashleft<=0) { // should be ==0
270 if (dc.length==0) { dc=dashes.slice(0); }
274 if (segleft<=0) { // should be ==0
275 node = way.getNode(i);
276 nextNode = way.getNode(i+1);
277 curx=map.lon2coord(node.lon);
278 dx=map.lon2coord(nextNode.lon)-curx;
279 cury=map.latp2coord(node.latp);
280 dy=map.latp2coord(nextNode.latp)-cury;
281 a=Math.atan2(dy,dx); xc=Math.cos(a); yc=Math.sin(a);
282 segleft=Math.sqrt(dx*dx+dy*dy);
286 if (segleft<=dashleft) {
287 // the path segment is shorter than the dash
289 moveLine(g,curx,cury,draw);
290 dashleft-=segleft; segleft=0;
292 // the path segment is longer than the dash
293 curx+=dashleft*xc; dx-=dashleft*xc;
294 cury+=dashleft*yc; dy-=dashleft*yc;
295 moveLine(g,curx,cury,draw);
296 segleft-=dashleft; dashleft=0;
301 private function moveLine(g:Graphics,x:Number,y:Number,draw:Boolean):void {
302 if (draw) { g.lineTo(x,y); }
303 else { g.moveTo(x,y); }
307 // Find point partway (0-1) along a path
308 // returns (x,y,angle)
309 // inspired by senocular's Path.as
311 private function pointAt(t:Number):Array {
312 var totallen:Number = t*pathlength;
313 var curlen:Number = 0;
314 var dx:Number, dy:Number, seglen:Number;
315 for (var i:int = 1; i < way.length; i++){
316 dx=map.lon2coord(way.getNode(i).lon)-map.lon2coord(way.getNode(i-1).lon);
317 dy=map.latp2coord(way.getNode(i).latp)-map.latp2coord(way.getNode(i-1).latp);
318 seglen=Math.sqrt(dx*dx+dy*dy);
319 if (totallen > curlen+seglen) { curlen+=seglen; continue; }
320 return new Array(map.lon2coord(way.getNode(i-1).lon)+(totallen-curlen)/seglen*dx,
321 map.latp2coord(way.getNode(i-1).latp)+(totallen-curlen)/seglen*dy,
324 return new Array(0, 0, 0);
327 // Draw name along path
328 // based on code by Tom Carden
330 private function writeNameOnPath(s:Sprite,a:String,textOffset:Number=0):void {
332 // make a dummy textfield so we can measure its width
333 var tf:TextField = new TextField();
334 tf.defaultTextFormat = nameformat;
336 tf.width = tf.textWidth+4;
337 tf.height = tf.textHeight+4;
338 if (pathlength<tf.width) { return; } // no room for text?
340 var t1:Number = (pathlength/2 - tf.width/2) / pathlength; var p1:Array=pointAt(t1);
341 var t2:Number = (pathlength/2 + tf.width/2) / pathlength; var p2:Array=pointAt(t2);
343 var angleOffset:Number; // so we can do a 180ยบ if we're running backwards
344 var offsetSign:Number; // -1 if we're starting at t2
345 var tStart:Number; // t1 or t2
347 // make sure text doesn't run right->left or upside down
350 p1[2] > -Math.PI/2) {
351 angleOffset = 0; offsetSign = 1; tStart = t1;
353 angleOffset = Math.PI; offsetSign = -1; tStart = t2;
356 // make a textfield for each char, centered on the line,
357 // using getCharBoundaries to rotate it around its center point
358 var chars:Array = a.split('');
359 for (var i:int = 0; i < chars.length; i++) {
360 var rect:Rectangle = tf.getCharBoundaries(i);
362 s.addChild(rotatedLetter(chars[i],
363 tStart + offsetSign*(rect.left+rect.width/2)/pathlength,
364 rect.width, tf.height, angleOffset, textOffset));
369 private function rotatedLetter(char:String, t:Number, w:Number, h:Number, a:Number, o:Number):TextField {
370 var tf:TextField = new TextField();
371 tf.mouseEnabled = false;
372 tf.mouseWheelEnabled = false;
373 tf.defaultTextFormat = nameformat;
374 tf.embedFonts = true;
376 tf.width = tf.textWidth+4;
377 tf.height = tf.textHeight+4;
379 var p:Array=pointAt(t);
380 var matrix:Matrix = new Matrix();
381 matrix.translate(-w/2, -h/2-o);
382 // ** add (say) -4 to the height to move it up by 4px
383 matrix.rotate(p[2]+a);
384 matrix.translate(p[0], p[1]);
385 tf.transform.matrix = matrix;
389 public function getNodeAt(x:Number, y:Number):Node {
390 for (var i:uint = 0; i < way.length; i++) {
391 var node:Node = way.getNode(i);
392 var nodeX:Number = map.lon2coord(node.lon);
393 var nodeY:Number = map.latp2coord(node.latp);
394 if ( nodeX >= x-NODESIZE && nodeX <= x+NODESIZE &&
395 nodeY >= y-NODESIZE && nodeY <= y+NODESIZE )
401 override protected function mouseEvent(event:MouseEvent):void {
402 var node:Node = getNodeAt(event.localX, event.localY);
404 map.entityMouseEvent(event, way);
406 map.entityMouseEvent(event, node);