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 var nodeSelected:int=stateClasses["nodeSelected"];
198 for (var i:uint = 0; i < way.length; i++) {
199 var node:Node = way.getNode(i);
200 nodetags=node.getTagsCopy();
201 if (i==0) { nodetags['_heading']= heading[i]; }
202 else { nodetags['_heading']=(heading[i]+heading[i-1])/2; }
203 if (highlight) { nodetags[':selectedway']='yes'; }
204 if (node.id==nodeSelected) { nodetags[':selected']='yes'; }
205 sl=map.ruleset.getStyles(node,nodetags);
206 if (sl.hasStyles()) {
207 if (!map.pois[node.id]) { map.pois[node.id]=new NodeUI(node,map,r); }
208 map.pois[node.id].redraw(sl);
209 // ** this should be done via the registerPOI/event listener mechanism,
210 // but that needs a bit of reworking so we can pass in a styleList
211 // (otherwise we end up computing the styles twice which is expensive)
212 } else if (map.pois[node.id]) {
213 map.pois[node.id].removeSprites();
216 if (!drawn) { return; }
218 // create a generic "way" hitzone sprite
219 hitzone = new Sprite();
220 hitzone.graphics.lineStyle(4, 0x000000, 1, false, "normal", CapsStyle.ROUND, JointStyle.ROUND);
221 solidLine(hitzone.graphics);
222 addToLayer(hitzone, CLICKSPRITE);
223 hitzone.visible = false;
224 createListenSprite(hitzone);
228 // ------------------------------------------------------------------------------------------
229 // Drawing support functions
231 private function drawNodes(g:Graphics):void {
232 // ***** these should be discreet anchorpoints (NodeUI?), not just sprites
233 // g.lineStyle(1, 0xff0000, 1, false, "normal", CapsStyle.ROUND, JointStyle.ROUND);
234 g.beginFill(0xFF0000);
235 for (var i:uint = 0; i < way.length; i++) {
236 var node:Node = way.getNode(i);
237 var x:Number = map.lon2coord(node.lon);
238 var y:Number = map.latp2coord(node.latp);
239 g.moveTo(x-NODESIZE, y-NODESIZE);
240 g.lineTo(x+NODESIZE, y-NODESIZE);
241 g.lineTo(x+NODESIZE, y+NODESIZE);
242 g.lineTo(x-NODESIZE, y+NODESIZE);
243 g.lineTo(x-NODESIZE, y-NODESIZE);
247 // Draw solid polyline
249 public function solidLine(g:Graphics):void {
250 var node:Node = way.getNode(0);
251 g.moveTo(map.lon2coord(node.lon), map.latp2coord(node.latp));
252 for (var i:uint = 1; i < way.length; i++) {
253 node = way.getNode(i);
254 g.lineTo(map.lon2coord(node.lon), map.latp2coord(node.latp));
258 // Draw dashed polyline
260 private function dashedLine(g:Graphics,dashes:Array):void {
261 var draw:Boolean=false, dashleft:Number=0, dc:Array=new Array();
262 var a:Number, xc:Number, yc:Number;
263 var curx:Number, cury:Number;
264 var dx:Number, dy:Number, segleft:Number=0;
267 var node:Node = way.getNode(0);
268 var nextNode:Node = way.getNode(0);
269 g.moveTo(map.lon2coord(node.lon), map.latp2coord(node.latp));
270 while (i < way.length-1 || segleft>0) {
271 if (dashleft<=0) { // should be ==0
272 if (dc.length==0) { dc=dashes.slice(0); }
276 if (segleft<=0) { // should be ==0
277 node = way.getNode(i);
278 nextNode = way.getNode(i+1);
279 curx=map.lon2coord(node.lon);
280 dx=map.lon2coord(nextNode.lon)-curx;
281 cury=map.latp2coord(node.latp);
282 dy=map.latp2coord(nextNode.latp)-cury;
283 a=Math.atan2(dy,dx); xc=Math.cos(a); yc=Math.sin(a);
284 segleft=Math.sqrt(dx*dx+dy*dy);
288 if (segleft<=dashleft) {
289 // the path segment is shorter than the dash
291 moveLine(g,curx,cury,draw);
292 dashleft-=segleft; segleft=0;
294 // the path segment is longer than the dash
295 curx+=dashleft*xc; dx-=dashleft*xc;
296 cury+=dashleft*yc; dy-=dashleft*yc;
297 moveLine(g,curx,cury,draw);
298 segleft-=dashleft; dashleft=0;
303 private function moveLine(g:Graphics,x:Number,y:Number,draw:Boolean):void {
304 if (draw) { g.lineTo(x,y); }
305 else { g.moveTo(x,y); }
309 // Find point partway (0-1) along a path
310 // returns (x,y,angle)
311 // inspired by senocular's Path.as
313 private function pointAt(t:Number):Array {
314 var totallen:Number = t*pathlength;
315 var curlen:Number = 0;
316 var dx:Number, dy:Number, seglen:Number;
317 for (var i:int = 1; i < way.length; i++){
318 dx=map.lon2coord(way.getNode(i).lon)-map.lon2coord(way.getNode(i-1).lon);
319 dy=map.latp2coord(way.getNode(i).latp)-map.latp2coord(way.getNode(i-1).latp);
320 seglen=Math.sqrt(dx*dx+dy*dy);
321 if (totallen > curlen+seglen) { curlen+=seglen; continue; }
322 return new Array(map.lon2coord(way.getNode(i-1).lon)+(totallen-curlen)/seglen*dx,
323 map.latp2coord(way.getNode(i-1).latp)+(totallen-curlen)/seglen*dy,
326 return new Array(0, 0, 0);
329 // Draw name along path
330 // based on code by Tom Carden
332 private function writeNameOnPath(s:Sprite,a:String,textOffset:Number=0):void {
334 // make a dummy textfield so we can measure its width
335 var tf:TextField = new TextField();
336 tf.defaultTextFormat = nameformat;
338 tf.width = tf.textWidth+4;
339 tf.height = tf.textHeight+4;
340 if (pathlength<tf.width) { return; } // no room for text?
342 var t1:Number = (pathlength/2 - tf.width/2) / pathlength; var p1:Array=pointAt(t1);
343 var t2:Number = (pathlength/2 + tf.width/2) / pathlength; var p2:Array=pointAt(t2);
345 var angleOffset:Number; // so we can do a 180ยบ if we're running backwards
346 var offsetSign:Number; // -1 if we're starting at t2
347 var tStart:Number; // t1 or t2
349 // make sure text doesn't run right->left or upside down
352 p1[2] > -Math.PI/2) {
353 angleOffset = 0; offsetSign = 1; tStart = t1;
355 angleOffset = Math.PI; offsetSign = -1; tStart = t2;
358 // make a textfield for each char, centered on the line,
359 // using getCharBoundaries to rotate it around its center point
360 var chars:Array = a.split('');
361 for (var i:int = 0; i < chars.length; i++) {
362 var rect:Rectangle = tf.getCharBoundaries(i);
364 s.addChild(rotatedLetter(chars[i],
365 tStart + offsetSign*(rect.left+rect.width/2)/pathlength,
366 rect.width, tf.height, angleOffset, textOffset));
371 private function rotatedLetter(char:String, t:Number, w:Number, h:Number, a:Number, o:Number):TextField {
372 var tf:TextField = new TextField();
373 tf.mouseEnabled = false;
374 tf.mouseWheelEnabled = false;
375 tf.defaultTextFormat = nameformat;
376 tf.embedFonts = true;
378 tf.width = tf.textWidth+4;
379 tf.height = tf.textHeight+4;
381 var p:Array=pointAt(t);
382 var matrix:Matrix = new Matrix();
383 matrix.translate(-w/2, -h/2-o);
384 // ** add (say) -4 to the height to move it up by 4px
385 matrix.rotate(p[2]+a);
386 matrix.translate(p[0], p[1]);
387 tf.transform.matrix = matrix;
391 public function getNodeAt(x:Number, y:Number):Node {
392 for (var i:uint = 0; i < way.length; i++) {
393 var node:Node = way.getNode(i);
394 var nodeX:Number = map.lon2coord(node.lon);
395 var nodeY:Number = map.latp2coord(node.latp);
396 if ( nodeX >= x-NODESIZE && nodeX <= x+NODESIZE &&
397 nodeY >= y-NODESIZE && nodeY <= y+NODESIZE )
403 override protected function mouseEvent(event:MouseEvent):void {
404 // var node:Node = getNodeAt(event.localX, event.localY);
405 // if ( node == null )
406 map.entityMouseEvent(event, way);
408 // map.entityMouseEvent(event, node);