drag multiple items
[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.TextField;
8         import flash.text.TextFormat;
9         import flash.events.*;
10         import net.systemeD.halcyon.styleparser.*;
11     import net.systemeD.halcyon.connection.*;
12         import net.systemeD.halcyon.Globals;
13
14         /** The graphical representation of a Way. */ 
15         public class WayUI extends EntityUI {
16
17                 /** Total length of way */
18                 public var pathlength:Number;
19                 /** Area of the way */
20                 public var patharea:Number;
21                 /** X coord of the centre of the area */
22                 public var centroid_x:Number;
23                 /** Y coord of the centre of the area */
24                 public var centroid_y:Number;                           //  |
25                 /** Angle at each node */
26                 public var heading:Array=new Array();
27                 /** vertex to draw exclusively, or not at all (used by DragWayNode) */ 
28                 public var drawExcept:Number;
29                 /** " */
30                 public var drawOnly:Number;
31                 private var indexStart:uint;
32                 private var indexEnd:uint;
33                 public var nameformat:TextFormat;
34                 private var recalculateDue:Boolean=false;
35                 // Store the temporary highlight settings applied to all nodes.
36                 private var nodehighlightsettings: Object={};
37
38                 private const NODESIZE:uint=6;
39
40                 public function WayUI(way:Way, paint:MapPaint) {
41                         super(way,paint);
42             entity.addEventListener(Connection.WAY_NODE_ADDED, wayNodeAdded);
43             entity.addEventListener(Connection.WAY_NODE_REMOVED, wayNodeRemoved);
44             entity.addEventListener(Connection.WAY_REORDERED, wayReordered);
45             entity.addEventListener(Connection.ENTITY_DRAGGED, wayDragged);
46             attachNodeListeners();
47             attachRelationListeners();
48             recalculate();
49                         redraw();
50                         redrawMultis();
51                 }
52                 
53                 public function removeEventListeners():void {
54                         removeGenericEventListeners();
55             entity.removeEventListener(Connection.WAY_NODE_ADDED, wayNodeAdded);
56             entity.removeEventListener(Connection.WAY_NODE_REMOVED, wayNodeRemoved);
57             entity.removeEventListener(Connection.WAY_REORDERED, wayReordered);
58             entity.removeEventListener(Connection.ENTITY_DRAGGED, wayDragged);
59             for (var i:uint = 0; i < Way(entity).length; i++ ) {
60                 Way(entity).getNode(i).removeEventListener(Connection.NODE_MOVED, nodeMoved);
61             }
62                 }
63                 
64                 private function attachNodeListeners():void {
65                         var way:Way=entity as Way;
66             for (var i:uint = 0; i < way.length; i++ ) {
67                 way.getNode(i).addEventListener(Connection.NODE_MOVED, nodeMoved);
68             }
69                 }
70                 
71                 private function wayNodeAdded(event:WayNodeEvent):void {
72                     event.node.addEventListener(Connection.NODE_MOVED, nodeMoved);
73             recalculate();
74                     redraw();
75                         redrawMultis();
76                 }
77                     
78                 private function wayNodeRemoved(event:WayNodeEvent):void {
79                     if (!event.node.hasParent(event.way)) {
80                                 event.node.removeEventListener(Connection.NODE_MOVED, nodeMoved);
81                         }
82                         if (paint.nodeuis[event.node.id]) {
83                                 paint.nodeuis[event.node.id].redraw();
84                         }
85             recalculate();
86                     redraw();
87                         redrawMultis();
88                 }
89                     
90         private function nodeMoved(event:NodeMovedEvent):void {
91                         recalculate();
92             redraw();
93                         redrawMultis();
94         }
95         private function wayReordered(event:EntityEvent):void {
96             redraw();
97                         redrawMultis();
98         }
99                 private function wayDragged(event:EntityDraggedEvent):void {
100                         offsetSprites(event.xDelta,event.yDelta);
101                 }
102
103                 override protected function relationAdded(event:RelationMemberEvent):void {
104                         super.relationAdded(event);
105                         redrawMultis();
106                 }
107                 override protected function relationRemoved(event:RelationMemberEvent):void {
108                         super.relationRemoved(event);
109                         redrawMultis();
110                 }
111                 override protected function relationTagChanged(event:TagEvent):void {
112                         super.relationTagChanged(event);
113                         redrawMultis();
114                 }
115
116                 /** Don't redraw until further notice. */
117                 override public function suspendRedraw(event:EntityEvent):void {
118                         super.suspendRedraw(event);
119                         recalculateDue=false;
120                 }
121                 
122                 /** Continue redrawing as normal. */
123                 override public function resumeRedraw(event:EntityEvent):void {
124                         suspended=false;
125                         if (recalculateDue) { recalculate(); }
126                         super.resumeRedraw(event);
127                 }
128
129                 /** Redraw other ways that are the "outer" part of a multipolygon of which we are the "inner" */
130                 public function redrawMultis():void {
131                         var multis:Array=entity.findParentRelationsOfType('multipolygon','inner');
132                         for each (var m:Relation in multis) {
133                                 var outers:Array=m.findMembersByRole('outer');
134                                 for each (var e:Entity in outers) { 
135                                         if (e is Way && paint.wayuis[e.id]) {
136                                                 paint.wayuis[e.id].redraw();
137                                         }
138                                 }
139                         }
140                 }
141         
142         /** Mark every node in this way as highlighted, and redraw it. 
143         * 
144         * @param settings Style state that it applies to. @see EntityUI#setStateClass()
145         *  */
146         public function setHighlightOnNodes(settings:Object):void {
147                         for (var i:uint = 0; i < Way(entity).length; i++) {
148                 var node:Node = Way(entity).getNode(i);
149                                 if (paint.nodeuis[node.id]) {
150                                         // Speed things up a bit by only setting the highlight if it's either:
151                                         // a) an "un-highlight" (so we don't leave mess behind when scrolling)
152                                         // b) currently onscreen
153                                         // Currently this means if you highlight an object then scroll, nodes will scroll
154                                         // into view that should be highlighted but aren't.
155                                         if (settings.hoverway==false || 
156                                             settings.selectedway==false || 
157                                             node.lat >=  paint.map.edge_b && node.lat <= paint.map.edge_t &&
158                                             node.lon >= paint.map.edge_l && node.lon <= paint.map.edge_r) {
159                                             paint.nodeuis[node.id].setHighlight(settings); // Triggers redraw if required
160                                         }
161                                         if (settings.selectedway  || settings.hoverway)
162                                                 nodehighlightsettings=settings;
163                                         else
164                                            nodehighlightsettings={}; 
165                                 }
166                         }
167         }
168         
169         // An ugly hack to allow nodes that have recently scrolled into view to get highlighted.
170         public function updateHighlights():void {
171                 if (nodehighlightsettings)
172                    setHighlightOnNodes(nodehighlightsettings);
173         }
174
175                 // ------------------------------------------------------------------------------------------
176                 /** Calculate pathlength, patharea, centroid_x, centroid_y, heading[]. 
177                 * ** this could be made scale-independent - would speed up redraw
178                 */
179                 public function recalculate():void {
180                         if (suspended) { recalculateDue=true; return; }
181                         
182                         var lx:Number, ly:Number, sc:Number;
183                         var node:Node, latp:Number, lon:Number;
184                         var cx:Number=0, cy:Number=0;
185                         var way:Way=entity as Way;
186                         
187                         pathlength=0;
188                         patharea=0;
189                         if (way.length==0) { return; }
190                         
191                         lx = way.getNode(way.length-1).lon;
192                         ly = way.getNode(way.length-1).latp;
193                         for ( var i:uint = 0; i < way.length; i++ ) {
194                 node = way.getNode(i);
195                 latp = node.latp;
196                 lon  = node.lon;
197
198                                 // length and area
199                                 if ( i>0 ) { pathlength += Math.sqrt( Math.pow(lon-lx,2)+Math.pow(latp-ly,2) ); }
200                                 sc = (lx*latp-lon*ly)*paint.map.scalefactor;
201                                 cx += (lx+lon)*sc;
202                                 cy += (ly+latp)*sc;
203                                 patharea += sc;
204                                 
205                                 // heading
206                                 if (i>0) { heading[i-1]=Math.atan2((lon-lx),(latp-ly)); }
207
208                                 lx=lon; ly=latp;
209                         }
210                         heading[way.length-1]=heading[way.length-2];
211
212                         pathlength*=paint.map.scalefactor;
213                         patharea/=2;
214                         if (patharea!=0 && way.isArea()) {
215                                 centroid_x=paint.map.lon2coord(cx/patharea/6);
216                                 centroid_y=paint.map.latp2coord(cy/patharea/6);
217                         } else if (pathlength>0) {
218                                 var c:Array=pointAt(0.5);
219                                 centroid_x=c[0];
220                                 centroid_y=c[1];
221                         }
222                 }
223
224                 // ------------------------------------------------------------------------------------------
225         
226                 /** Go through the complex process of drawing this way, including applying styles, casings, fills, fonts... */
227                 override public function doRedraw():Boolean {
228                         interactive=false;
229                         removeSprites();
230                         if (Way(entity).length==0) { return false; }
231                         if (!paint.ready) { return false; }
232
233             // Copy tags object, and add states
234             var tags:Object = entity.getTagsCopy();
235             setStateClass('area', Way(entity).isArea());
236             setStateClass('tiger', (entity.isUneditedTiger() && Globals.vars.highlightTiger));
237             tags=applyStateClasses(tags);
238
239                         // Keep track of maximum stroke width for hitzone
240                         var maxwidth:Number=4;
241
242                         // Create styleList if not already drawn
243                         if (!styleList || !styleList.isValidAt(paint.map.scale)) {
244                                 styleList=paint.ruleset.getStyles(entity, tags, paint.map.scale);
245                         }
246
247                         // Which layer?
248                         layer=styleList.layerOverride();
249                         if (isNaN(layer)) {
250                                 layer=0;
251                                 if (tags['layer']) { layer=Math.min(Math.max(tags['layer'],paint.minlayer),paint.maxlayer); }
252                         }
253
254                         // Do we have to draw all nodes in the way?
255                         if (isNaN(drawOnly)) {
256                                 indexStart=0; indexEnd=Way(entity).length; 
257                         } else {
258                                 indexStart=Math.max(0,drawOnly-1);
259                                 indexEnd  =Math.min(drawOnly+2,Way(entity).length);
260                         }
261
262                         // Iterate through each sublayer, drawing any styles on that layer
263                         var drawn:Boolean;
264                         var multis:Array=entity.findParentRelationsOfType('multipolygon','outer');
265                         var inners:Array=[];
266                         for each (var m:Relation in multis) {
267                                 inners=inners.concat(m.findMembersByRole('inner',Way));
268                         }
269
270                         for each (var sublayer:Number in styleList.sublayers) {
271                                 if (styleList.shapeStyles[sublayer]) {
272                                         var s:ShapeStyle=styleList.shapeStyles[sublayer];
273                                         var stroke:Shape, fill:Shape, casing:Shape, roadname:Sprite;
274                                         var x0:Number=paint.map.lon2coord(Way(entity).getNode(0).lon);
275                                         var y0:Number=paint.map.latp2coord(Way(entity).getNode(0).latp);
276                                         interactive||=s.interactive;
277
278                                         // Stroke
279                                         if (s.width)  {
280                                                 stroke=new Shape(); addToLayer(stroke,STROKESPRITE,sublayer);
281                                                 stroke.graphics.moveTo(x0,y0);
282                                                 s.applyStrokeStyle(stroke.graphics);
283                                                 if (s.dashes && s.dashes.length>0) {
284                                                         var segments:Array=dashedLine(stroke.graphics,s.dashes); 
285                                                         if (s.line_style) { lineDecoration(stroke.graphics,s,segments); }
286                                                 } else { solidLines(stroke.graphics,inners); }
287                                                 drawn=true;
288                                                 if (s.interactive) { maxwidth=Math.max(maxwidth,s.width); }
289                                         }
290
291                                         // Fill
292                                         if ((!isNaN(s.fill_color) || s.fill_image) && entity.findParentRelationsOfType('multipolygon','inner').length==0) {
293                                                 fill=new Shape(); addToLayer(fill,FILLSPRITE);
294                                                 fill.graphics.moveTo(x0,y0);
295                                                 if (s.fill_image) { new WayBitmapFiller(this,fill.graphics,s); }
296                                                                          else { s.applyFill(fill.graphics); }
297                                                 solidLines(fill.graphics,inners);
298                                                 fill.graphics.endFill();
299                                                 drawn=true;
300                                         }
301
302                                         // Casing
303                                         if (s.casing_width) { 
304                                                 casing=new Shape(); addToLayer(casing,CASINGSPRITE);
305                                                 casing.graphics.moveTo(x0,y0);
306                                                 s.applyCasingStyle(casing.graphics);
307                                                 if (s.casing_dashes && s.casing_dashes.length>0) { dashedLine(casing.graphics,s.casing_dashes); }
308                                                                                                                                         else { solidLines(casing.graphics,inners); }
309                                                 drawn=true;
310                                                 if (s.interactive) { maxwidth=Math.max(maxwidth,s.casing_width); }
311                                         }
312                                 }
313                                 
314                                 if (styleList.textStyles[sublayer]) {
315                                         var t:TextStyle=styleList.textStyles[sublayer];
316                                         interactive||=t.interactive;
317                                         roadname=new Sprite(); addToLayer(roadname,NAMESPRITE);
318                                         nameformat = t.getTextFormat();
319                                         var a:String=tags[t.text];
320                                         if (a) {
321                                                 if (t.font_caps) { a=a.toUpperCase(); }
322                                                 if (t.text_center && centroid_x) {
323                                                         t.writeNameLabel(roadname,a,centroid_x,centroid_y);
324                                                 } else {
325                                                         writeNameOnPath(roadname,a,t.text_offset ? t.text_offset : 0);
326                                                 }
327                                                 if (t.text_halo_radius>0) { roadname.filters=t.getHaloFilter(); }
328                                         }
329                                 }
330                                 
331                                 // ** ShieldStyle to do
332                         }
333
334                         // Draw icons
335                         var r:Number;
336                         var nodeSelected:int=stateClasses["nodeSelected"];
337                         for (var i:uint = indexStart; i < indexEnd; i++) {
338                 var node:Node = Way(entity).getNode(i);
339                                 var nodeStateClasses:Object={};
340 //                              if (i==0) { nodetags['_heading']= heading[i]; }
341 //                                   else { nodetags['_heading']=(heading[i]+heading[i-1])/2; }
342                                 // ** FIXME - heading isn't currently applied
343                                 nodeStateClasses['junction']=(node.numParentWays>1);
344                                 paint.createNodeUI(node,r,layer,nodeStateClasses);
345                         }
346                         if (!drawn) { return false; } // If not visible, no hitzone.
347                         
348             // create a generic "way" hitzone sprite
349                         if (interactive && drawn) {
350                     hitzone = new Sprite();
351                     hitzone.graphics.lineStyle(maxwidth, 0x000000, 1, false, "normal", CapsStyle.ROUND, JointStyle.ROUND);
352                     solidLines(hitzone.graphics,[]);
353                     hitzone.visible = false;
354                                 setListenSprite();
355                         }
356
357                         return true;
358                 }
359                 
360                 // ------------------------------------------------------------------------------------------
361                 // Drawing support functions
362
363                 /** Draw solid polyline */
364                 
365                 public function solidLines(g:Graphics,inners:Array):void {
366                         solidLine(g);
367                         for each (var w:Way in inners) { solidLineOtherWay(g,w); }
368                 }
369
370                 private function solidLine(g:Graphics):void {
371                         if (indexEnd==0) { return; }
372                         var way:Way=entity as Way;
373                         
374             var node:Node = way.getNode(indexStart);
375                         g.moveTo(paint.map.lon2coord(node.lon), paint.map.latp2coord(node.latp));
376                         for (var i:uint = indexStart+1; i < indexEnd; i++) {
377                 node = way.getNode(i);
378                                 if (!isNaN(drawExcept) && (i-1==drawExcept || i==drawExcept)) {
379                                         g.moveTo(paint.map.lon2coord(node.lon), paint.map.latp2coord(node.latp));
380                                 } else {
381                                         g.lineTo(paint.map.lon2coord(node.lon), paint.map.latp2coord(node.latp));
382                                 }
383                         }
384                 }
385
386                 private function solidLineOtherWay(g:Graphics,way:Way):void {
387                         if (way.length==0) { return; }
388                         
389                         var node:Node = way.getNode(indexStart);
390                         g.moveTo(paint.map.lon2coord(node.lon), paint.map.latp2coord(node.latp));
391                         for (var i:uint = 1; i < way.length; i++) {
392                                 node = way.getNode(i);
393                                 g.lineTo(paint.map.lon2coord(node.lon), paint.map.latp2coord(node.latp));
394                         }
395                 }
396
397                 /** Draw dashed polyline */
398                 
399                 private function dashedLine(g:Graphics,dashes:Array):Array {
400                         var way:Way=entity as Way;
401                         var segments:Array=[];
402                         var draw:Boolean=false, dashleft:Number=0, dc:Array=new Array();
403                         var a:Number, xc:Number, yc:Number;
404                         var curx:Number, cury:Number;
405                         var dx:Number, dy:Number, segleft:Number=0;
406                         var i:int=indexStart;
407
408             var node:Node = way.getNode(i);
409             var nextNode:Node = way.getNode(i);
410                         g.moveTo(paint.map.lon2coord(node.lon), paint.map.latp2coord(node.latp));
411                         while (i < indexEnd-1 || segleft>0) {
412                                 if (dashleft<=0) {      // should be ==0
413                                         if (dc.length==0) { dc=dashes.slice(0); }
414                                         dashleft=dc.shift();
415                                         if (draw) { segments.push([curx,cury,dx,dy]); }
416                                         draw=!draw;
417                                 }
418                                 if (i==drawExcept || i==drawExcept+1) { draw=false; }
419                                 if (segleft<=0) {       // should be ==0
420                     node = way.getNode(i);
421                     nextNode = way.getNode(i+1);
422                                         curx=paint.map.lon2coord(node.lon);
423                     dx=paint.map.lon2coord(nextNode.lon)-curx;
424                                         cury=paint.map.latp2coord(node.latp);
425                     dy=paint.map.latp2coord(nextNode.latp)-cury;
426                                         a=Math.atan2(dy,dx); xc=Math.cos(a); yc=Math.sin(a);
427                                         segleft=Math.sqrt(dx*dx+dy*dy);
428                                         i++;
429                                 }
430
431                                 if (segleft<=dashleft) {
432                                         // the path segment is shorter than the dash
433                                         curx+=dx; cury+=dy;
434                                         moveLine(g,curx,cury,draw);
435                                         dashleft-=segleft; segleft=0;
436                                 } else {
437                                         // the path segment is longer than the dash
438                                         curx+=dashleft*xc; dx-=dashleft*xc;
439                                         cury+=dashleft*yc; dy-=dashleft*yc;
440                                         moveLine(g,curx,cury,draw);
441                                         segleft-=dashleft; dashleft=0;
442                                 }
443                         }
444                         return segments;
445                 }
446
447                 private function moveLine(g:Graphics,x:Number,y:Number,draw:Boolean):void {
448                         if (draw) { g.lineTo(x,y); }
449                                  else { g.moveTo(x,y); }
450                 }
451
452                 /** Draw decoration (arrows etc.) */
453                 
454                 private function lineDecoration(g:Graphics,s:ShapeStyle,segments:Array):void {
455                         var c:int=s.color ? s.color : 0;
456                         switch (s.line_style.toLowerCase()) {
457
458                                 case 'arrows':
459                                         var w:Number=s.width*1.5;       // width of arrow
460                                         var l:Number=s.width*2;         // length of arrow
461                                         var angle0:Number, angle1:Number, angle2:Number;
462                                         g.lineStyle(1,c);
463                                         for each (var seg:Array in segments) {
464                                                 g.beginFill(c);
465                                                 angle0= Math.atan2(seg[3],seg[2]);
466                                                 angle1=-Math.atan2(seg[3],seg[2]);
467                                                 angle2=-Math.atan2(seg[3],seg[2])-Math.PI;
468                                                 g.moveTo(seg[0]+l*Math.cos(angle0),
469                                                          seg[1]+l*Math.sin(angle0));
470                                                 g.lineTo(seg[0]+w*Math.sin(angle1),
471                                                          seg[1]+w*Math.cos(angle1));
472                                                 g.lineTo(seg[0]+w*Math.sin(angle2),
473                                                          seg[1]+w*Math.cos(angle2));
474                                                 g.endFill();
475                                         }
476                                         break;
477                                 }
478                 }
479
480                 
481                 /** Find point partway (0-1) along a path
482                   * @return (x,y,angle)
483                   * inspired by senocular's Path.as */
484                 
485                 private function pointAt(t:Number):Array {
486                         var way:Way=entity as Way;
487                         var totallen:Number = t*pathlength;
488                         var curlen:Number = 0;
489                         var dx:Number, dy:Number, seglen:Number;
490                         for (var i:int = 1; i < way.length; i++){
491                                 dx=paint.map.lon2coord(way.getNode(i).lon)-paint.map.lon2coord(way.getNode(i-1).lon);
492                                 dy=paint.map.latp2coord(way.getNode(i).latp)-paint.map.latp2coord(way.getNode(i-1).latp);
493                                 seglen=Math.sqrt(dx*dx+dy*dy);
494                                 if (totallen > curlen+seglen) { curlen+=seglen; continue; }
495                                 return new Array(paint.map.lon2coord(way.getNode(i-1).lon)+(totallen-curlen)/seglen*dx,
496                                                                  paint.map.latp2coord(way.getNode(i-1).latp)+(totallen-curlen)/seglen*dy,
497                                                                  Math.atan2(dy,dx));
498                         }
499                         return new Array(0, 0, 0);
500                 }
501
502                 /** Draw name along path
503                  * based on code by Tom Carden
504                  * */
505                 
506                 private function writeNameOnPath(s:Sprite,a:String,textOffset:Number=0):void {
507
508                         // make a dummy textfield so we can measure its width
509                         var tf:TextField = new TextField();
510                         tf.defaultTextFormat = nameformat;
511                         tf.text = a;
512                         tf.width = tf.textWidth+4;
513                         tf.height = tf.textHeight+4;
514                         if (pathlength<tf.width) { return; }    // no room for text?
515
516                         var t1:Number = (pathlength/2 - tf.width/2) / pathlength; var p1:Array=pointAt(t1);
517                         var t2:Number = (pathlength/2 + tf.width/2) / pathlength; var p2:Array=pointAt(t2);
518
519                         var angleOffset:Number; // so we can do a 180ยบ if we're running backwards
520                         var offsetSign:Number;  // -1 if we're starting at t2
521                         var tStart:Number;      // t1 or t2
522
523                         // make sure text doesn't run right->left or upside down
524                         if (p1[0] < p2[0] && 
525                                 p1[2] < Math.PI/2 &&
526                                 p1[2] > -Math.PI/2) {
527                                 angleOffset = 0; offsetSign = 1; tStart = t1;
528                         } else {
529                                 angleOffset = Math.PI; offsetSign = -1; tStart = t2;
530                         } 
531
532                         // make a textfield for each char, centered on the line,
533                         // using getCharBoundaries to rotate it around its center point
534                         var chars:Array = a.split('');
535                         for (var i:int = 0; i < chars.length; i++) {
536                                 var rect:Rectangle = tf.getCharBoundaries(i);
537                                 if (rect) {
538                                         s.addChild(rotatedLetter(chars[i],
539                                                                                          tStart + offsetSign*(rect.left+rect.width/2)/pathlength,
540                                                                                          rect.width, tf.height, angleOffset, textOffset));
541                                 }
542                         }
543                 }
544
545                 private function rotatedLetter(char:String, t:Number, w:Number, h:Number, a:Number, o:Number):TextField {
546                         var tf:TextField = new TextField();
547             tf.mouseEnabled = false;
548             tf.mouseWheelEnabled = false;
549                         tf.defaultTextFormat = nameformat;
550                         tf.embedFonts = true;
551                         tf.text = char;
552                         tf.width = tf.textWidth+4;
553                         tf.height = tf.textHeight+4;
554
555                         var p:Array=pointAt(t);
556                         var matrix:Matrix = new Matrix();
557                         matrix.translate(-w/2, -h/2-o);
558                         // ** add (say) -4 to the height to move it up by 4px
559                         matrix.rotate(p[2]+a);
560                         matrix.translate(p[0], p[1]);
561                         tf.transform.matrix = matrix;
562                         return tf;
563                 }
564                 
565                 public function getNodeAt(x:Number, y:Number):Node {
566                         var way:Way=entity as Way;
567                         for (var i:uint = 0; i < way.length; i++) {
568                 var node:Node = way.getNode(i);
569                 var nodeX:Number = paint.map.lon2coord(node.lon);
570                 var nodeY:Number = paint.map.latp2coord(node.latp);
571                 if ( nodeX >= x-NODESIZE && nodeX <= x+NODESIZE &&
572                      nodeY >= y-NODESIZE && nodeY <= y+NODESIZE )
573                     return node;
574             }
575             return null;
576                 }
577
578                 // ------------------------------------------------------------------------------------------
579                 /* Interaction */
580         // TODO: can this be sped up? Hit testing for long ways (that go off the screen) seems to be very slow. */
581                 public function hitTest(x:Number, y:Number):Way {
582                         if (hitzone.hitTestPoint(x,y,true)) { return entity as Way; }
583                         return null;
584                 }
585         }
586 }