root/trunk/as3/lib/com/modestmaps/core/TileGrid.as

Revision 543, 41.8 kB (checked in by allens, 1 month ago)

Allowed the screen coordinate conversion methods take a DisplayObject? instance as the context instead of Sprite.

  • Property svn:keywords set to Id
Line 
1 /*
2  * vim:et sts=4 sw=4 cindent:
3  * $Id$
4  */
5
6 package com.modestmaps.core
7 {
8     import com.modestmaps.Map;
9     import com.modestmaps.geo.Location;
10     import com.modestmaps.mapproviders.IMapProvider;
11     import com.stamen.twisted.*;
12    
13     import flash.display.DisplayObject;
14     import flash.display.Sprite;
15     import flash.events.Event;
16     import flash.events.MouseEvent;
17     import flash.geom.Point;
18     import flash.geom.Rectangle;
19     import flash.utils.Dictionary;
20    
21     public class TileGrid extends Sprite
22     {
23         // Real maps use 256.
24         public static const TILE_WIDTH:Number = 256;
25         public static const TILE_HEIGHT:Number = 256;
26    
27         protected var _map:Map;
28    
29         protected var _width:Number;
30         protected var _height:Number;
31         protected var _draggable:Boolean;       
32    
33         // Row and column counts are kept up-to-date.
34         protected var _rows:int;
35         protected var _columns:int;
36         protected var _tiles:/*Tile*/Array;
37        
38         // overlay markers
39         protected var markers:MarkerSet;
40        
41         // Markers overlapping the currently-included set of tiles, hash of booleans
42         protected var _overlappingMarkers:Dictionary;
43    
44         // Allow (true) or prevent (false) tiles to paint themselves.
45         protected var _paintingAllowed:Boolean;
46        
47         // Starting point for the very first tile
48         protected var _initTilePoint:Point;
49         protected var _initTileCoord:Coordinate;
50        
51         // the currently-native zoom level
52         public var zoomLevel:int;
53        
54         // some limits on scrolling distance, initially set to none
55         protected var topLeftOutLimit:Coordinate;
56         protected var bottomRightInLimit:Coordinate;
57        
58         protected var _startingWellPosition:Point;
59    
60         // Tiles attach to the well.
61         protected var _well:Sprite;
62        
63         // Mask clip to hide outside edges of tiles.
64         protected var _mask:Sprite;
65    
66         // Active when the well is being dragged on the stage.
67         protected var _wellDragTask:DelayedCall;
68        
69         // Defines a ring of extra, masked-out tiles around
70         // the edges of the well, acting as a pre-fetching cache.
71         // High tileBuffer may hurt performance.
72         protected var _tileBuffer:int = 0;
73    
74         // Who do we get our Map graphics from?
75         protected var _mapProvider:IMapProvider;
76    
77         protected var _drawWell:Boolean = true;
78         protected var _drawGridArea:Boolean = true;
79    
80         public function TileGrid(width:Number, height:Number, draggable:Boolean, provider:IMapProvider, map:Map)
81         {
82             if (!Reactor.running())
83                 throw new Error('com.modestmaps.core.TileGrid.init(): com.stamen.Twisted.Reactor really ought to be running at this point. Seriously.');
84    
85             _map = map;
86             _width = width;
87             _height = height;
88             _draggable = draggable;
89             _mapProvider = provider;
90        
91             cacheAsBitmap = true;
92                
93             buildWell();
94             buildMask();
95             allowPainting(true);
96             redraw();   
97            
98             _overlappingMarkers = new Dictionary(true);
99             markers = new MarkerSet(this);
100            
101             setInitialTile(new Coordinate(0,0,1), new Point(-TILE_WIDTH, -TILE_HEIGHT));
102             initializeTiles();
103         }
104        
105        /**
106         * Set initTileCoord and initTilePoint for use by initializeTiles().
107         */
108         public function setInitialTile(coord:Coordinate, point:Point):void
109         {
110             _initTileCoord = coord;
111             _initTilePoint = point;
112         }
113        
114        /**
115         * Reset tile grid with a new initial tile, and expire old tiles in the background.
116         */
117         public function resetTiles(coord:Coordinate, point:Point):void
118         {
119             if (!_tiles)
120                 {
121                 setInitialTile(coord, point);
122                 return;
123             }
124        
125             try {
126                 var initTile:Tile;
127                 var condemnedTiles:/*Tile*/Array = activeTiles();
128    
129                 for (var i:int = 0; i < condemnedTiles.length; i++)
130                 {
131                     condemnedTiles[i].expire();
132                 }
133    
134                     Reactor.callLater(condemnationDelay(), destroyTiles, condemnedTiles);
135
136                 zoomLevel = coord.zoom;               
137                 initTile = createTile(this, coord, point.x, point.y);
138                                                                      
139                 centerWell(true);
140    
141                 _rows = 1;
142                 _columns = 1;
143    
144                 allocateTiles();
145             }
146             catch(e:Error) {
147                 trace(e.getStackTrace());
148             }
149            
150         }
151        
152        /**
153         * Create the first tiles, based on initTileCoord and initTilePoint.
154         */
155         protected function initializeTiles():void
156         {
157             var initTile:Tile;
158            
159             if (!_initTileCoord) {
160                 trace("no _initTileCoord");
161                 return;           
162             }           
163                          
164             // impose some limits
165             zoomLevel = _initTileCoord.zoom;
166             topLeftOutLimit = _mapProvider.outerLimits()[0];
167             bottomRightInLimit = _mapProvider.outerLimits()[1];
168            
169             _tiles = [];
170             initTile = createTile(this, _initTileCoord, _initTilePoint.x, _initTilePoint.y);
171                                                                      
172             centerWell(false);
173    
174             _rows = 1;
175             _columns = 1;
176            
177             // buffer must not be negative!
178             _tileBuffer = Math.max(0, _tileBuffer);
179            
180             allocateTiles();
181            
182             // let 'em know we're coming
183             markers.indexAtZoom(zoomLevel);
184            
185             updateMarkers();
186         }
187        
188         public function putMarker(id:String, coord:Coordinate, location:Location):Marker
189         {
190             var marker:Marker = new Marker(id, coord, location);
191             markers.put(marker);
192    
193             updateMarkers();
194             return marker;
195         }
196    
197         public function removeMarker(id:String):void
198         {
199             var marker:Marker = markers.getMarker(id);
200             if (marker)
201                 markers.remove(marker);
202         }
203        
204        /**
205         * Create the well clip, assign event handlers.
206         */
207         protected function buildWell():void
208         {
209             _well = new Sprite();
210             _well.name = 'well';
211            
212             if (_draggable)
213             {
214                 _well.mouseChildren = false;
215                 _well.addEventListener(MouseEvent.MOUSE_DOWN, startWellDrag);
216                 _well.addEventListener(MouseEvent.MOUSE_UP, stopWellDrag);
217                 _well.doubleClickEnabled = true;
218             }
219            
220             addChild(_well);           
221             centerWell(false);
222         }
223        
224        /**
225         * Create the mask clip.
226         */
227         protected function buildMask():void
228         {
229             _mask = new Sprite();
230             _mask.name = 'mask';
231             // as3 masks need to be child, so add the mask to the grid not the well
232             // because well children are all tiles
233             addChild(_mask);
234             this.mask = _mask;
235         }
236
237                 public function setDoubleClickEnabled(enabled:Boolean):void
238                 {
239                         if (enabled) {
240                 _well.addEventListener(MouseEvent.DOUBLE_CLICK, onWellDoubleClick);
241                         }
242                         else if (_well.hasEventListener(MouseEvent.DOUBLE_CLICK)) {
243                                 _well.removeEventListener(MouseEvent.DOUBLE_CLICK, onWellDoubleClick);
244                         }
245                 }       
246        
247         public function getMapProvider():IMapProvider
248         {
249             return _mapProvider;
250         }
251    
252         public function setMapProvider(mapProvider:IMapProvider):void
253         {
254             var previousGeometry:String = _mapProvider.geometry();
255    
256             _mapProvider = mapProvider;
257             topLeftOutLimit = _mapProvider.outerLimits()[0];
258             bottomRightInLimit = _mapProvider.outerLimits()[1];
259    
260             if (_mapProvider.geometry() != previousGeometry)
261             {
262                 markers.initializeIndex();
263                 markers.indexAtZoom(zoomLevel);
264                 updateMarkers();
265             }
266         }
267        
268        
269        /**
270         * Create a new tile, add it to _tiles array, and return it.
271         */
272         protected function createTile(grid:TileGrid, coord:Coordinate, x:Number, y:Number):Tile
273         {
274             var tile:Tile = new Tile(grid, coord, x, y);
275             tile.name = 'tile' + _tiles.length;
276             _well.addChild(tile);
277                        
278             tile.redraw();
279             _tiles.push(tile);
280            
281             return tile;
282         }
283    
284        /**
285         * Remove an old tile from the _tiles array, then destroy it.
286         */
287         protected function destroyTile(tile:Tile):void
288         {
289             _tiles.splice(tileIndex(tile), 1);
290             tile.cancelDraw();
291             _well.removeChild(tile);
292         }
293        
294        /*
295         * Slowly mete out destruction to a list of tiles.
296         */
297         protected function destroyTiles(tiles:/*Tile*/Array):void
298         {
299             if (tiles.length)
300             {
301                 destroyTile(Tile(tiles.shift()));
302                 Reactor.callLater(0, destroyTiles, tiles);
303             }
304         }
305    
306        /*
307         * Reposition tiles and schedule a recursive call for the next frame.
308         */
309         protected function onWellDrag(previousPosition:Point):void
310         {
311             if(positionTiles())
312                 updateMarkers();
313    
314             if(previousPosition.x != _well.x || previousPosition.y != _well.y)
315                 _map.onPanned(new Point(_well.x - _startingWellPosition.x, _well.y - _startingWellPosition.y));
316            
317             _wellDragTask = Reactor.callNextFrame(onWellDrag, new Point(_well.x, _well.y));
318         }
319        
320        /*
321         * Return the point position of a tile with the given coordinate in the
322         * context of the given movie clip.
323         *
324         * Respect infinite rows or columns, to bind movement on one (or no) axis.
325         */
326         public function coordinatePoint(coord:Coordinate, context:DisplayObject, fearBigNumbers:Boolean=false):Point
327         {
328             // pick a reference tile, an arbitrary choice
329             // but known to exist regardless of grid size.
330             var tile:Tile = activeTiles()[0];
331        
332             // get the position of the reference tile.
333             var point:Point = new Point(tile.x, tile.y);
334            
335             // make sure coord is using the same zoom level
336             coord = coord.zoomTo(tile.coord.zoom);
337            
338             // store the infinite
339             var force:Point = new Point(0, 0);
340            
341             if(coord.column == Number.POSITIVE_INFINITY || coord.column == Number.NEGATIVE_INFINITY) {
342                 force.x = coord.column;
343             } else {
344                 point.x += TILE_WIDTH * (coord.column - tile.coord.column);           
345             }
346            
347             if(coord.row == Number.POSITIVE_INFINITY || coord.row == Number.NEGATIVE_INFINITY) {
348                 force.y = coord.row;
349             } else {
350                 point.y += TILE_HEIGHT * (coord.row - tile.coord.row);
351             }
352            
353             if(fearBigNumbers) {
354                 if(point.x < -1e6) {
355                     force.x = Number.NEGATIVE_INFINITY;
356                 }
357                 if(point.x > 1e6) {
358                     force.x = Number.POSITIVE_INFINITY;
359                 }
360                 if(point.y < -1e6) {
361                     force.y = Number.NEGATIVE_INFINITY;
362                 }
363                 if(point.y > 1e6) {
364                     force.y = Number.POSITIVE_INFINITY;
365                 }
366             }
367            
368             point = _well.localToGlobal(point);
369             point = context.globalToLocal(point);
370    
371             if(force.x) {
372                 point.x = force.x;
373             }
374             if(force.y) {
375                 point.y = force.y;
376             }
377             return point;
378         }
379        
380         public function pointCoordinate(point:Point, context:DisplayObject=null):Coordinate
381         {
382             var tile:Tile;
383             var tileCoord:Coordinate;
384             var pointCoord:Coordinate;
385            
386             if (null == context) context = this;
387             // point is assumed to be in tile grid local coordinates
388             point = context.localToGlobal(point);
389             point = _well.globalToLocal(point);
390    
391             // an arbitrary reference tile, zoomed to the maximum
392             tile = activeTiles()[0];
393             tileCoord = tile.coord.zoomTo(Coordinate.MAX_ZOOM);
394            
395             // distance in tile widths from reference tile to point
396             var xTiles:Number = (point.x - tile.x) / TILE_WIDTH;
397             var yTiles:Number = (point.y - tile.y) / TILE_HEIGHT;
398    
399             // distance in rows & columns at maximum zoom
400             var xDistance:Number = xTiles * Math.pow(2, (Coordinate.MAX_ZOOM - tile.coord.zoom));
401             var yDistance:Number = yTiles * Math.pow(2, (Coordinate.MAX_ZOOM - tile.coord.zoom));
402            
403             // new point coordinate reflecting that distance
404             pointCoord = new Coordinate(Math.round(tileCoord.row + yDistance),
405                                         Math.round(tileCoord.column + xDistance),
406                                         tileCoord.zoom);
407            
408             return pointCoord.zoomTo(tile.coord.zoom);
409         }
410        
411         public function topLeftCoordinate():Coordinate
412         {
413             var point:Point = new Point(0, 0);
414             return pointCoordinate(point);
415         }
416        
417         public function centerCoordinate():Coordinate
418         {
419             var point:Point = new Point(_width/2, _height/2);
420             return pointCoordinate(point);
421         }
422        
423         public function bottomRightCoordinate():Coordinate
424         {
425             var point:Point = new Point(_width, _height);
426             return pointCoordinate(point);
427         }
428        
429        /*
430         * Start dragging the well with the mouse.
431         * Calls onWellDrag().
432         */
433         protected function getWellBounds(fearBigNumbers:Boolean):Bounds
434         {
435             var min:Point, max:Point;
436    
437             // "min" = furthest well position left & up,
438             // use the location of the bottom-right limit
439             min = coordinatePoint(bottomRightInLimit, this, fearBigNumbers);
440             min.x = _well.x - min.x + _width;
441             min.y = _well.y - min.y + _height;
442            
443             // "max" = furthest well position right & down,
444             // use the location of the top-left limit
445             max = coordinatePoint(topLeftOutLimit, this, fearBigNumbers);
446             max.x = _well.x - max.x;
447             max.y = _well.y - max.y;
448                        
449             // weird negative edge conditions, limit all movement on an axis
450             if(min.x > max.x)
451                 min.x = max.x = _well.x;
452    
453             if(min.y > max.y)
454                 min.y = max.y = _well.y;
455                
456             return new Bounds(min, max);
457         }
458
459         private function onWellDoubleClick(event:MouseEvent):void
460         {
461             var p:Point = new Point(event.localX, event.localY);
462             var l:Location = _map.pointLocation(p,_well);
463             _map.panTo(l);
464         }
465        
466        /*
467         * Start dragging the well with the mouse.
468         * Calls onWellDrag().
469         */
470         public function startWellDrag(event:MouseEvent):void
471         {
472             stage.addEventListener(MouseEvent.MOUSE_UP, stopWellDrag);           
473             stage.addEventListener(Event.MOUSE_LEAVE, stopWellDrag);
474
475             var bounds:Bounds = getWellBounds(true);
476            
477             // startDrag seems to hate the infinities,
478             // so we'll fudge it with some implausibly large numbers.
479            
480             var xMin:Number = (bounds.min.x == Number.POSITIVE_INFINITY)
481                                 ? 100000
482                                 : ((bounds.min.x == Number.NEGATIVE_INFINITY)
483                                     ? -100000
484                                     : bounds.min.x);
485            
486             var yMin:Number = (bounds.min.y == Number.POSITIVE_INFINITY)
487                                 ? 100000
488                                 : ((bounds.min.y == Number.NEGATIVE_INFINITY)
489                                     ? -100000
490                                     : bounds.min.y);
491            
492             var xMax:Number = (bounds.max.x == Number.POSITIVE_INFINITY)
493                                 ? 100000
494                                 : ((bounds.max.x == Number.NEGATIVE_INFINITY)
495                                     ? -100000
496                                     : bounds.max.x);
497            
498             var yMax:Number = (bounds.max.y == Number.POSITIVE_INFINITY)
499                                 ? 100000
500                                 : ((bounds.max.y == Number.NEGATIVE_INFINITY)
501                                     ? -100000
502                                     : bounds.max.y);
503                                    
504             _startingWellPosition = new Point(_well.x, _well.y);
505            
506             _map.onStartPan();
507             var rect:Rectangle = new Rectangle(xMin, yMin, xMax - xMin, yMax - yMin);
508             _well.startDrag(false, rect);
509             onWellDrag(_startingWellPosition.clone());
510         }
511        
512        /*
513         * Stop dragging the well with the mouse.
514         * Halts _wellDragTask.
515         */
516         public function stopWellDrag(event:Event):void
517         {
518             stage.removeEventListener(MouseEvent.MOUSE_UP, stopWellDrag);           
519             stage.removeEventListener(Event.MOUSE_LEAVE, stopWellDrag);
520
521             if (_wellDragTask) {
522                 _wellDragTask.call();   // issue final onPan, notify markers, etc.
523                 _wellDragTask.cancel(); // but cancel the follow-on call
524             }
525             _map.onStopPan();
526             _well.stopDrag();
527    
528             if(positionTiles())
529                 updateMarkers();
530    
531             centerWell(true);
532         }
533        
534         public function zoomBy(amount:Number, redraw:Boolean):void
535         {
536             if(!_tiles)
537                 return;
538            
539             var roundScale:Number = Math.round(_well.scaleX * 10000.0) / 10000.0;
540             if(amount > 0 && zoomLevel >= bottomRightInLimit.zoom && roundScale)
541                 return;
542        
543             if(amount < 0 && zoomLevel <= topLeftOutLimit.zoom && roundScale)
544                 return;
545        
546             _well.scaleX *= Math.pow(2, amount);
547             _well.scaleY *= Math.pow(2, amount);
548            
549             boundWell();
550            
551             if(redraw) {
552                 normalizeWell();
553                 allocateTiles();
554                     //trace('New well scale: '+_well.scaleX.toString());
555             }
556         }
557        
558         public function resizeTo(bottomRight:Point):void
559         {
560             _width = bottomRight.x;
561             _height = bottomRight.y;
562    
563             redraw();
564    
565             if(!_tiles)
566                 return;
567            
568             centerWell(false);
569             allocateTiles();
570         }
571        
572         public function panRight(pixels:Number):void
573         {
574             if(!_tiles)
575                 return;
576            
577             _well.x -= pixels;
578    
579             if(positionTiles())
580                 updateMarkers();
581    
582             centerWell(true);
583         }
584      
585         public function panLeft(pixels:Number):void
586         {
587             if(!_tiles)
588                 return;
589            
590             _well.x += pixels;
591    
592             if(positionTiles())
593                 updateMarkers();
594    
595             centerWell(true);
596         }
597      
598         public function panUp(pixels:Number):void
599         {
600             if(!_tiles)
601                 return;
602            
603             _well.y += pixels;
604    
605             if(positionTiles())
606                 updateMarkers();
607    
608             centerWell(true);
609         }     
610        
611         public function panDown(pixels:Number):void
612         {
613             if(!_tiles)
614                 return;
615            
616             _well.y -= pixels;
617    
618             if(positionTiles())
619                 updateMarkers();
620    
621             centerWell(true);
622         }
623    
624        /**
625         * Get the subset of still-active tiles.
626         */
627         protected function activeTiles():/*Tile*/Array
628         {
629             var matches:Array = new Array();
630             if (_tiles) {
631                 matches = _tiles.filter(function(item:Tile, index:int, list:Array):Boolean { return item.isActive();} );
632                 if (matches.length == 0) {
633                     trace("no matches for active tiles... DOOM!");
634                 }
635             }
636             return matches;
637         }
638    
639        /**
640         * Find the given tile in the tiles array.
641         */
642         protected function tileIndex(tile:Tile):Number
643         {
644             return _tiles.indexOf(tile);
645         }
646    
647        /**
648         * Determine the number of tiles needed to cover the current grid,
649         * and add rows and columns if necessary. Finally, position new tiles.
650         */
651         protected function allocateTiles():void
652         {
653             if(!_tiles)
654                 return;
655            
656             // internal pixel dimensions of well, compensating for scale
657             var wellWidth:Number  = _well.scaleX * _width;
658             var wellHeight:Number = _well.scaleY * _height;
659    
660             var targetCols:Number = Math.ceil(wellWidth  / TILE_WIDTH)  + 1 + 2 * _tileBuffer;
661             var targetRows:Number = Math.ceil(wellHeight / TILE_HEIGHT) + 1 + 2 * _tileBuffer;
662    
663             // grid can't drop below 1 x 1
664             targetCols = Math.max(1, targetCols);
665             targetRows = Math.max(1, targetRows);
666    
667             // change column count to match target
668             while(_columns != targetCols) {
669                 if(_columns < targetCols) {
670                     pushTileColumn();
671                 } else if(_columns > targetCols) {
672                     popTileColumn();
673                 }
674             }
675    
676             // change row count to match target
677             while(_rows != targetRows) {
678                 if(_rows < targetRows) {
679                     pushTileRow();
680                 } else if(_rows > targetRows) {
681                     popTileRow();
682                 }
683             }
684    
685             if(positionTiles())
686                 updateMarkers();
687                
688             //trace("allocateTiles(): " + _tiles.length);
689         }
690        
691        /**
692         * Adjust position of the well, so it does not stray outside the provider boundaries.
693         */
694         protected function boundWell():void
695         {
696             var bounds:Bounds = getWellBounds(true);
697            
698             _well.x = Math.min(bounds.max.x, Math.max(bounds.min.x, _well.x));
699             _well.y = Math.min(bounds.max.y, Math.max(bounds.min.y, _well.y));
700         }
701        
702        /**
703         * Adjust position of the well, so it stays in the center.
704         * Optionally, compensate tile positions to prevent
705         * visual discontinuity.
706         */
707         protected function centerWell(adjustTiles:Boolean):void
708         {
709             var center:Point = new Point(_width/2, _height/2);
710            
711             var xAdjustment:Number = _well.x - center.x;
712             var yAdjustment:Number = _well.y - center.y;
713    
714             _well.x -= xAdjustment;
715             _well.y -= yAdjustment;
716            
717             if(!_tiles)
718                 return;
719            
720             if(adjustTiles) {
721                 for (var i:int = 0; i < _tiles.length; i += 1) {
722                     _tiles[i].x += xAdjustment / _well.scaleX;
723                     _tiles[i].y += yAdjustment / _well.scaleX;
724                 }
725             }
726         }
727        
728        /**
729         * Adjust position and scale of the well, so it stays
730         * in the center and within reason.  Compensate tile
731         * zoom and positions to prevent visual discontinuity.
732         */
733         protected function normalizeWell():void
734         {
735             //trace("normalizing well");
736             if(!_tiles) {
737                 return;
738             }
739            
740             var zoomAdjust:Number, scaleAdjust:Number;
741             var active:/*Tile*/Array;
742            
743             // just in case?
744             centerWell(true);
745    
746                 //trace("well scale: " + _well.scaleX + " " + _well.scaleY);
747             if(Math.abs(_well.scaleX - 1.0) < 0.01) {
748                 active = activeTiles();
749            
750                 // set to 100% if within 99% - 101%
751                 //trace("scaling well to 100% from " + _well.scaleX*100 + "%");
752                 _well.scaleX = _well.scaleY = 1.0;
753                
754                 active.sort(compareTileRowColumn);
755                
756                 // lock the tiles back to round-pixel positions
757                 active[0].x = Math.floor(active[0].x);
758                 active[0].y = Math.floor(active[0].y);
759                
760                 for(var i:int = 1; i < active.length; i += 1) {
761                     active[i].x = active[0].x + (active[i].coord.column - active[0].coord.column) * TILE_WIDTH;
762                     active[i].y = active[0].y + (active[i].coord.row    - active[0].coord.row)    * TILE_HEIGHT;
763                
764                     //trace(active[i].toString()+' at '+active[i].x+', '+active[i].y+' vs. '+active[0].toString());
765                 }
766    
767             } else if(_well.scaleX <= 0.6 || _well.scaleX >= 1.65) {
768                 // split or merge tiles if outside of 60% - 165%
769    
770                 // zoom adjust: base-2 logarithm of the scale
771                 // see http://mathworld.wolfram.com/Logarithm.html (15)
772                 zoomAdjust = Math.round(Math.log(_well.scaleX) / Math.log(2));
773                 scaleAdjust = Math.pow(2, zoomAdjust);
774            
775                 //trace('This is where we scale the whole well by '+zoomAdjust+' zoom levels: '+(100 / scaleAdjust)+'%');
776
777                 var n:int;
778                 for (n  = 0; n < zoomAdjust; n += 1)
779                 {
780                     splitTiles();
781                     zoomLevel += 1;
782                 }
783                    
784                 for (n = 0; n > zoomAdjust; n -= 1)
785                 {
786                     mergeTiles();
787                     zoomLevel -= 1;
788                 }
789    
790                 _well.scaleX = _well.scaleX / scaleAdjust;
791                 _well.scaleY = _well.scaleY / scaleAdjust;
792    
793                 for (var j:int = 0; j < _tiles.length; j += 1) {
794                     _tiles[j].x = _tiles[j].x * scaleAdjust;
795                     _tiles[j].y = _tiles[j].y * scaleAdjust;
796                     _tiles[j].scaleX = _tiles[j].scaleX * scaleAdjust;
797                     _tiles[j].scaleY = _tiles[j].scaleY * scaleAdjust;
798                 }
799            
800                 //trace('Scaled to '+zoomLevel+', '+(_well.scaleX*100.0)+'%');
801                 markers.indexAtZoom(zoomLevel);
802             }
803         }
804        
805        /**
806         * How many milliseconds before condemned tiles are destroyed?
807         */
808         protected function condemnationDelay():Number
809         {
810             // half a second for each tile, plus five seconds overhead
811             return (5 + .5 * _rows * _columns) * 1000;
812         }
813        
814        /**
815         * Do a 1-to-4 tile split: pick a reference tile and use it
816         * as a position for four new tiles at a higher zoom level.
817         * Expire all existing tiles, and trust that allocateTiles() and
818         * positionTiles() will take care of filling the remaining space.
819         */
820         protected function splitTiles():void
821         {
822             //trace("splitting tiles");
823             var condemnedTiles:/*Tile*/Array = [];
824             var referenceTile:Tile, newTile:Tile;
825             var xOffset:Number, yOffset:Number;
826            
827             for(var i:int = _tiles.length - 1; i >= 0; i -= 1) {
828                 if(_tiles[i].isActive()) {
829                     // remove old tile
830                     _tiles[i].expire();
831                     condemnedTiles.push(_tiles[i]);
832    
833                     // save for later (you only need one)
834                     referenceTile = _tiles[i];
835                 }
836             }
837    
838             Reactor.callLater(condemnationDelay(), destroyTiles, condemnedTiles);
839
840             // this should never happen
841             if(!referenceTile) {
842                 trace("TileGrid problem - no reference tile");
843                 return;
844             }
845        
846             // this should never happen either
847             if(!referenceTile.coord) {
848                 trace("TileGrid problem - no coord in reference tile");
849                 return;
850             }
851    
852             for(var q:Number = 0; q < 4; q += 1) {
853                 // two-bit value into two one-bit values
854                 xOffset = q & 1;
855                 yOffset = (q >> 1) & 1;
856                
857                 newTile = createTile(referenceTile.grid, referenceTile.coord, referenceTile.x, referenceTile.y);
858                 newTile.coord = newTile.coord.zoomBy(1);
859                
860                 if(xOffset)
861                     newTile.coord = newTile.coord.right();
862                
863                 if(yOffset)
864                     newTile.coord = newTile.coord.down();
865    
866                 newTile.x = referenceTile.x + (xOffset * TILE_WIDTH / 2);
867                 newTile.y = referenceTile.y + (yOffset * TILE_HEIGHT / 2);
868
869                 newTile.scaleX = newTile.scaleY = referenceTile.scaleX / 2;
870                 newTile.redraw();
871             }
872    
873             // The remaining tiles get taken care of later
874             _rows = 2;
875             _columns = 2;
876         }
877        
878        /**
879         * Do a 4-to-1 tile merge: pick a reference tile and use it
880         * as a position for the upper-left-hand corder of one new tile
881         * at a higher zoom level. Expire all existing tiles, and trust
882         * that allocateTiles() and positionTiles() will take care of
883         * filling the remaining space.
884         */
885         protected function mergeTiles():void
886         {
887             //trace("merging tiles");
888             var condemnedTiles:/*Tile*/Array = [];
889             var referenceTile:Tile, newTile:Tile;
890        
891             _tiles.sort(compareTileRowColumn);
892    
893             for(var i:int = _tiles.length - 1; i >= 0; i -= 1) {
894                 if(_tiles[i].isActive()) {
895                     // remove old tile
896                     _tiles[i].expire();
897                     condemnedTiles.push(_tiles[i]);
898    
899                     if(_tiles[i].coord.zoomBy(-1).isEdge()) {
900                         // save for later (you only need one)
901                         referenceTile = _tiles[i];
902                     }
903                 }
904             }
905    
906             Reactor.callLater(condemnationDelay(), destroyTiles, condemnedTiles);
907        
908             // this should never happen
909             if(!referenceTile) {
910                 throw new Error("no reference tile in mergeTiles()");
911             }
912
913             // this should never happen either
914             if(!referenceTile.coord) {
915                 throw new Error("no reference tile coord in mergeTiles()");
916             }
917    
918             // we are only interested in tiles that are edges for this zoom
919             newTile = createTile(referenceTile.grid, referenceTile.coord, referenceTile.x, referenceTile.y);
920             newTile.coord = newTile.coord.zoomBy(-1);
921                
922             newTile.scaleX = newTile.scaleY = referenceTile.scaleX * 2;
923             newTile.redraw();
924    
925             // The remaining tiles get taken care of later
926             _rows = 1;
927             _columns = 1;
928         }
929        
930        /**
931         * Determine if any tiles have wandered too far to the right, left,
932         * top, or bottom, and shunt them to the opposite side if needed.
933         * Return true if any tiles have been repositioned.
934         */
935         protected function positionTiles():Boolean
936         {
937             if(!_tiles)
938                 return false;
939            
940             var tile:Tile;
941             var point:Point;
942             var active:/*Tile*/Array = activeTiles();
943            
944             // if any tile is moved...
945             var touched:Boolean = false;
946            
947             point = new Point(0, 0);
948             point = this.localToGlobal(point);
949             point = _well.globalToLocal(point); // all tiles are attached to well
950            
951             var xMin:Number = point.x - (1 + _tileBuffer) * TILE_WIDTH;
952             var yMin:Number = point.y - (1 + _tileBuffer) * TILE_HEIGHT;
953            
954             point = new Point(_width, _height);
955             point = this.localToGlobal(point);
956             point = _well.globalToLocal(point); // all tiles are attached to well
957            
958             var xMax:Number = point.x + (0 + _tileBuffer) * TILE_WIDTH;
959             var yMax:Number = point.y + (0 + _tileBuffer) * TILE_HEIGHT;
960            
961             for(var i:int = 0; i < active.length; i += 1) {
962            
963                 tile = active[i];
964                
965                 // only interested in moving active tiles
966                 if(!tile.isActive())
967                     break; // shouldn't happen, TODO: perhaps a case for throwing an Error?
968                
969                 if(tile.y < yMin) {
970                     // too far up
971                     tile.panDown(_rows);
972                     tile.y += _rows * TILE_HEIGHT;
973                     touched = true;
974    
975                 } else if(tile.y > yMax) {
976                     // too far down
977                     if((tile.y - _rows * TILE_HEIGHT) > yMin) {
978                         // moving up wouldn't put us too far
979                         tile.panUp(_rows);
980                         tile.y -= _rows * TILE_HEIGHT;
981                         touched = true;
982                     }
983                 }
984                
985                 if(tile.x < xMin) {
986                     // too far left
987                     tile.panRight(_columns);
988                     tile.x += _columns * TILE_WIDTH;
989                     touched = true;
990    
991                 } else if(tile.x > xMax) {
992                     // too far right
993                     if((tile.x - _columns * TILE_WIDTH) > xMin) {
994                         // moving left wouldn't put us too far
995                         tile.panLeft(_columns);
996                         tile.x -= _columns * TILE_WIDTH;
997                         touched = true;
998                     }
999                 }
1000