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

Revision 736, 52.2 kB (checked in by tom, 6 months ago)

fix as3 crossdomain check to only fire if we're caching loaders or smoothing content... also fixed swc

  • Property svn:keywords set to Id
Line 
1 package com.modestmaps.core
2 {
3         import com.modestmaps.events.MapEvent;
4         import com.modestmaps.mapproviders.IMapProvider;
5        
6         import flash.display.Bitmap;
7         import flash.display.DisplayObject;
8         import flash.display.Loader;
9         import flash.display.LoaderInfo;
10         import flash.display.Sprite;
11         import flash.events.Event;
12         import flash.events.IOErrorEvent;
13         import flash.events.MouseEvent;
14         import flash.events.ProgressEvent;
15         import flash.events.TimerEvent;
16         import flash.geom.Matrix;
17         import flash.geom.Point;
18         import flash.geom.Rectangle;
19         import flash.net.URLRequest;
20         import flash.system.LoaderContext;
21         import flash.system.System;
22         import flash.text.TextField;
23         import flash.text.TextFormat;
24         import flash.utils.Dictionary;
25         import flash.utils.Timer;
26         import flash.utils.getTimer;
27
28         public class TileGrid extends Sprite
29         {
30                 // OPTIONS
31                 ///////////////////////////////
32                
33                 // TODO: split these out into a TileGridOptions class and allow mass setting/getting?
34                
35                 protected static const DEFAULT_MAX_PARENT_SEARCH:int = 5;
36                 protected static const DEFAULT_MAX_PARENT_LOAD:int = 0; // enable this to load lower zoom tiles first
37                 protected static const DEFAULT_MAX_CHILD_SEARCH:int = 1;
38                 protected static const DEFAULT_MAX_TILES_TO_KEEP:int = 256; // 256*256*4bytes = 0.25MB ... so 256 tiles is 64MB of memory, minimum!
39                 protected static const DEFAULT_TILE_BUFFER:int = 0;
40                 protected static const DEFAULT_ENFORCE_BOUNDS:Boolean = true;
41                 protected static const DEFAULT_MAX_OPEN_REQUESTS:int = 4; // TODO: should this be split into max-new-requests-per-frame, too?
42                 protected static const DEFAULT_ROUND_POSITIONS:Boolean = true;
43                 protected static const DEFAULT_ROUND_SCALES:Boolean = true;
44                 protected static const DEFAULT_CACHE_LOADERS:Boolean = false;  // !!! only enable this if you have crossdomain permissions to access Loader content
45                 protected static const DEFAULT_SMOOTH_CONTENT:Boolean = false; // !!! only enable this if you have crossdomain permissions to access Loader content
46                 protected static const DEFAULT_MAX_LOADER_CACHE_SIZE:int = 0; // !!! suggest 256 or so
47
48                 /** if we don't have a tile at currentZoom, onRender will look for tiles up to 5 levels out.
49                  *  set this to 0 if you only want the current zoom level's tiles
50                  *  WARNING: tiles will get scaled up A LOT for this, but maybe it beats blank tiles? */
51                 public var maxParentSearch:int = DEFAULT_MAX_PARENT_SEARCH;
52                 /** if we don't have a tile at currentZoom, onRender will look for tiles up to one levels further in.
53                  *  set this to 0 if you only want the current zoom level's tiles
54                  *  WARNING: bad, bad nasty recursion possibilities really soon if you go much above 1
55                  *  - it works, but you probably don't want to change this number :) */
56                 public var maxChildSearch:int = DEFAULT_MAX_CHILD_SEARCH;
57
58                 /** if maxParentSearch is enabled, setting maxParentLoad to between 1 and maxParentSearch
59                  *   will make requests for lower zoom levels first */
60                 public var maxParentLoad:int = DEFAULT_MAX_PARENT_LOAD;
61
62                 /** this is the maximum size of tileCache (visible tiles will also be kept in the cache) */             
63                 public var maxTilesToKeep:int = DEFAULT_MAX_TILES_TO_KEEP;
64                
65                 // 0 or 1, really: 2 will load *lots* of extra tiles
66                 public var tileBuffer:int = DEFAULT_TILE_BUFFER;
67                
68                 // how many Loaders are allowed to be open at once?
69                 public var maxOpenRequests:int = DEFAULT_MAX_OPEN_REQUESTS;
70
71                 /** set this to true to enable enforcing of map bounds from the map provider's limits */
72                 public var enforceBoundsEnabled:Boolean = DEFAULT_ENFORCE_BOUNDS;
73                
74                 /** set this to false, along with roundScalesEnabled, if you need a map to stay 'fixed' in place as it changes size */
75                 public var roundPositionsEnabled:Boolean = DEFAULT_ROUND_POSITIONS;
76                
77                 /** set this to false, along with roundPositionsEnabled, if you need a map to stay 'fixed' in place as it changes size */
78                 public var roundScalesEnabled:Boolean = DEFAULT_ROUND_SCALES;
79
80                 /** set this to true to enable bitmap smoothing on tiles - requires crossdomain.xml permissions so won't work online with most providers */
81                 public var smoothContent:Boolean = DEFAULT_SMOOTH_CONTENT;
82                
83                 /** with tile providers that you have crossdomain.xml support for,
84                  *  it's possible to avoid extra requests by reusing bitmapdata. enable cacheLoaders to try and do that */
85                 public static var cacheLoaders:Boolean = DEFAULT_CACHE_LOADERS;
86                 public static var maxLoaderCacheSize:int = DEFAULT_MAX_LOADER_CACHE_SIZE;
87                 protected static var loaderCache:Object = {};
88                 protected static var cachedUrls:Array = [];
89                
90                 ///////////////////////////////
91                 // END OPTIONS
92
93         // TILE_WIDTH and TILE_HEIGHT are now tileWidth and tileHeight
94         // this was needed for the NASA DailyPlanetProvider which has 512x512px tiles
95                 // public static const TILE_WIDTH:Number = 256;
96                 // public static const TILE_HEIGHT:Number = 256;       
97        
98         // read-only, kept up to date by calculateBounds()
99         protected var _minZoom:Number;
100         protected var _maxZoom:Number;
101
102                 protected var minTx:Number, maxTx:Number, minTy:Number, maxTy:Number;
103
104                 // read-only, convenience for tileWidth/Height
105                 protected var _tileWidth:Number;
106                 protected var _tileHeight:Number;
107
108                 // pan and zoom etc are stored in here
109                 // NB: this matrix is never applied to a DisplayObject's transform
110                 //     because it would require scaling tile positions to compensate.
111                 //     Instead, we adapt its values such that the current zoom level
112                 //     is approximately scale 1, and positions make sense in screen pixels
113                 protected var worldMatrix:Matrix;
114                
115                 // this turns screen points into coordinates
116                 protected var _invertedMatrix:Matrix; // use lazy getter for this
117                
118                 // the corners and center of the screen, in map coordinates
119                 // (these also have lazy getters)
120                 protected var _topLeftCoordinate:Coordinate;
121                 protected var _bottomRightCoordinate:Coordinate;
122                 protected var _centerCoordinate:Coordinate;
123
124                 // where the tiles live:
125                 protected var well:Sprite;
126
127                 protected var provider:IMapProvider;
128
129                 protected var tileQueue:TileQueue;
130
131                 protected var tileCache:TileCache;
132
133                 protected var tilePool:TilePool;
134                
135                 // per-tile, the array of images we're going to load, which can be empty
136                 // TODO: document this in IMapProvider, so that provider implementers know
137                 // they are free to check the bounds of their overlays and don't have to serve
138                 // millions of 404s
139                 protected var layersNeeded:Object = {};
140
141                 // keys we've recently seen
142                 protected var recentlySeen:Array = [];
143                
144                 // open requests
145                 protected var openRequests:Array = [];
146
147                 // keeping track for dispatching MapEvent.ALL_TILES_LOADED and MapEvent.BEGIN_TILE_LOADING
148                 protected var previousOpenRequests:int = 0;
149                
150                 // currently visible tiles
151                 protected var visibleTiles:Array = [];
152                                
153                 // number of tiles we're failing to show
154                 protected var blankCount:int = 0;
155
156                 // a textfield with lots of stats
157                 public var debugField:TextField;
158                
159                 // for stats:
160                 protected var lastFrameTime:Number;
161                 protected var fps:Number = 30;
162
163                 // what zoom level of tiles is 'correct'?
164                 protected var _currentTileZoom:int;
165                 // so we know if we're going in or out
166                 protected var previousTileZoom:int;             
167                
168                 // for sorting the queue:
169                 protected var centerRow:Number;
170                 protected var centerColumn:Number;
171
172                 // for pan events
173                 protected var startPan:Coordinate;
174                 public var panning:Boolean;
175                
176                 // previous mouse position when dragging
177                 protected var pmouse:Point;
178                
179                 // for zoom events
180                 protected var startZoom:Number = -1;
181                 public var zooming:Boolean;
182                
183                 protected var mapWidth:Number;
184                 protected var mapHeight:Number;
185                
186                 protected var draggable:Boolean;
187
188                 // setting this.dirty = true will request an Event.RENDER
189                 protected var _dirty:Boolean;
190
191                 // setting to true will dispatch a CHANGE event which Map will convert to an EXTENT_CHANGED for us
192                 protected var matrixChanged:Boolean = false;
193                
194                 protected var queueTimer:Timer;
195                
196                 public function TileGrid(w:Number, h:Number, draggable:Boolean, provider:IMapProvider)
197                 {
198                         doubleClickEnabled = true;
199                        
200                         //this.map = map;
201                         this.draggable = draggable;
202
203                         // don't call set map provider here, because it triggers a redraw and we're not ready for that
204                         this.provider = provider;
205                        
206                         // but do grab tile dimensions:
207                         _tileWidth = provider.tileWidth;
208                         _tileHeight = provider.tileHeight;
209
210                         // and calculate bounds from provider
211                         calculateBounds();
212                        
213                         this.tilePool = new TilePool(Tile);
214                         this.tileQueue = new TileQueue();
215                         this.tileCache = new TileCache(tilePool);
216
217                         this.mapWidth = w;
218                         this.mapHeight = h;
219
220                         debugField = new TextField();
221                         debugField.defaultTextFormat = new TextFormat(null, 12, 0x000000, false);
222                         debugField.backgroundColor = 0xffffff;
223                         debugField.background = true;
224                         debugField.text = "messages";
225                         debugField.x = mapWidth - debugField.width - 15;
226                         debugField.y = mapHeight - debugField.height - 15;
227                         debugField.name = 'text';
228                         debugField.mouseEnabled = false;
229                         debugField.selectable = false;
230                         debugField.multiline = true;
231                         debugField.wordWrap = false;
232                        
233                         lastFrameTime = getTimer();
234                        
235                         well = new Sprite();
236                         well.name = 'well';
237                         well.doubleClickEnabled = true;
238                         well.mouseEnabled = true;
239                         well.mouseChildren = false;
240                         addChild(well);
241
242                         worldMatrix = new Matrix();
243                        
244                         addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
245                        
246                         queueTimer = new Timer(200);
247                         queueTimer.addEventListener(TimerEvent.TIMER, processQueue);
248                 }
249                
250                 /**
251                  * Get the Tile instance that corresponds to a given coordinate.
252                  */
253                 public function getCoordTile(coord:Coordinate):Tile
254                 {
255                     // these get floored when they're cast as ints in tileKey()
256                     var key:String = tileKey(coord.column, coord.row, coord.zoom);
257                     return well.getChildByName(key) as Tile;
258                 }
259                
260                 private function onAddedToStage(event:Event):void
261                 {
262                         if (draggable) {
263                                 addEventListener(MouseEvent.MOUSE_DOWN, mousePressed, true);
264                         }
265                         addEventListener(Event.RENDER, onRender);
266                         addEventListener(Event.ENTER_FRAME, onEnterFrame);
267                         queueTimer.start();
268                         addEventListener(Event.REMOVED_FROM_STAGE, onRemovedFromStage);
269                         removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
270                         dirty = true;
271                         // force an on-render in case we were added in a render handler
272                         onRender();
273                 }
274                
275                 private function onRemovedFromStage(event:Event):void
276                 {
277                         if (hasEventListener(MouseEvent.MOUSE_DOWN)) {
278                                 removeEventListener(MouseEvent.MOUSE_DOWN, mousePressed, true);
279                         }
280                         removeEventListener(Event.RENDER, onRender);
281                         removeEventListener(Event.ENTER_FRAME, onEnterFrame);
282                         queueTimer.stop();
283                         removeEventListener(Event.REMOVED_FROM_STAGE, onRemovedFromStage);
284                         addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
285                 }
286
287                 /** The classes themselves serve as factories!
288                  *
289                  * @param tileClass e.g. Tile, TweenTile, etc.
290                  *
291                  * @see http://norvig.com/design-patterns/img013.gif 
292                  */
293                 public function setTileClass(tileClass:Class):void
294                 {
295                         tilePool.setTileClass(tileClass);
296                         clearEverything();
297                 }
298                
299                 /** processes the tileQueue and optionally outputs stats into debugField */
300                 protected function onEnterFrame(event:Event=null):void
301                 {
302                         if (debugField.parent) {
303                                 // for stats...
304                                 var frameDuration:Number = getTimer() - lastFrameTime;
305                                 lastFrameTime = getTimer();
306                                
307                                 fps = (0.9 * fps) + (0.1 * (1000.0/frameDuration));
308        
309                                 // report stats:
310                                 var tileChildren:int = 0;
311                                 for (var i:int = 0; i < well.numChildren; i++) {
312                                         tileChildren += Tile(well.getChildAt(i)).numChildren;
313                                 } 
314 /*                              debugField.text = "fps: " + fps.toFixed(0)
315                                                 + "\nmemory: " + (System.totalMemory/1048576).toFixed(1) + "MB"; */
316  
317                                 debugField.text = "tx: " + tx.toFixed(3)
318                                                 + "\nty: " + tx.toFixed(3)
319                                                 + "\nsc: " + scale.toFixed(4)
320                                                 + "\nfps: " + fps.toFixed(0)
321                                                 + "\ncurrent child count: " + well.numChildren
322                                                 + "\ncurrent child of tile count: " + tileChildren
323                                                 + "\nvisible tile count: " + visibleTiles.length
324                                                 + "\nqueue length: " + tileQueue.length
325                                                 + "\nblank count: " + blankCount
326                                                 + "\nrequests: " + openRequests.length
327                                                 + "\nfinished (cached) tiles: " + tileCache.size
328                                                 + "\nrecently used tiles: " + recentlySeen.length
329                                                 + "\ncachedLoaders: " + cachedUrls.length
330                                                 + "\nTiles created: " + Tile.count
331                                                 + "\nmemory: " + (System.totalMemory/1048576).toFixed(1) + "MB";
332                                 debugField.width = debugField.textWidth+8;
333                                 debugField.height = debugField.textHeight+4;
334                                 debugField.x = mapWidth - debugField.width - 15;
335                                 debugField.y = mapHeight - debugField.height - 15;
336                         }
337                        
338                         //processQueue();
339                 }
340                
341                 protected function onRendered():void
342                 {
343                         // listen out for this if you want to be sure map is in its final state before reprojecting markers etc.
344                         dispatchEvent(new MapEvent(MapEvent.RENDERED));
345                 }
346                
347                 protected function onPanned():void
348                 {
349                         var pt:Point = coordinatePoint(startPan);
350                         dispatchEvent(new MapEvent(MapEvent.PANNED, pt.subtract(new Point(mapWidth/2, mapHeight/2))));                 
351                 }
352                
353                 protected function onZoomed():void
354                 {
355                         var zoomEvent:MapEvent = new MapEvent(MapEvent.ZOOMED_BY, zoomLevel-startZoom);
356                         // this might also be useful
357                     zoomEvent.zoomLevel = zoomLevel;
358                 dispatchEvent(zoomEvent);                       
359                 }
360                
361                 protected function onChanged():void
362                 {
363                         // doesn't bubble, unlike MapEvent
364                         // Map will pick this up and dispatch MapEvent.EXTENT_CHANGED for us
365                         dispatchEvent(new Event(Event.CHANGE, false, false));                   
366                 }
367                
368                 protected function onBeginTileLoading():void
369                 {
370                         dispatchEvent(new MapEvent(MapEvent.BEGIN_TILE_LOADING));                       
371                 }
372                
373                 protected function onProgress():void
374                 {
375                     // dispatch tile load progress
376                     dispatchEvent(new ProgressEvent(ProgressEvent.PROGRESS, false, false, previousOpenRequests - openRequests.length, previousOpenRequests));                   
377                 }
378                
379                 protected function onAllTilesLoaded():void
380                 {
381                         dispatchEvent(new MapEvent(MapEvent.ALL_TILES_LOADED));                 
382                 }
383                
384                 /**
385                  * figures out from worldMatrix which tiles we should be showing, adds them to the stage, adds them to the tileQueue if needed, etc.
386                  *
387                  * from my recent testing, TileGrid.onRender takes < 5ms most of the time, and rarely >10ms
388                  * (Flash Player 9, Firefox, Macbook Pro)
389                  * 
390                  */
391                 protected function onRender(event:Event=null):void
392                 {
393                         if (!dirty || !stage) {
394                                 onRendered();
395                                 return;
396                         }
397
398                         var boundsEnforced:Boolean = enforceBounds();
399
400                         if (zooming || panning) {
401                                 if (panning) {
402                                         onPanned();
403                                 }       
404                                 if (zooming) {
405                                         onZoomed();
406                                 }
407                         }
408                         else if (boundsEnforced) {
409                                 onChanged();
410                         }
411                         else if (matrixChanged) {
412                                 matrixChanged = false;
413                                 onChanged();
414                         }
415                        
416                         // what zoom level of tiles should we be loading, taking into account min/max zoom?
417                         // (0 when scale == 1, 1 when scale == 2, 2 when scale == 4, etc.)
418                         var newZoom:int = Math.min(maxZoom, Math.max(minZoom, Math.round(zoomLevel)));
419                        
420                         // see if the newZoom is different to currentZoom
421                         // so we know which way we're zooming, if any:
422                         if (currentTileZoom != newZoom) {
423                                 previousTileZoom = currentTileZoom;
424                         }
425                        
426                         // this is the level of tiles we'll be loading:
427                         _currentTileZoom = newZoom;
428                
429                         // find start and end columns for the visible tiles, at current tile zoom
430                         // TODO: take account of potential rotation in worldMatrix (ask Tom about this if you need it)
431                         var tlC:Coordinate = topLeftCoordinate.zoomTo(currentTileZoom);
432                         var brC:Coordinate = bottomRightCoordinate.zoomTo(currentTileZoom);
433                        
434                         // optionally pad it out a little bit more with a tile buffer
435                         // TODO: investigate giving a directional bias to TILE_BUFFER when panning quickly
436                         // NB:- I'm pretty sure these calculations are accurate enough that using
437                         //      Math.ceil for the maxCols will load one column too many -- Tom
438                         var minCol:int = Math.floor(tlC.column) - tileBuffer;
439                         var maxCol:int = Math.floor(brC.column) + tileBuffer;
440                         var minRow:int = Math.floor(tlC.row) - tileBuffer;
441                         var maxRow:int = Math.floor(brC.row) + tileBuffer;
442
443                         // loop over all tiles and find parent or child tiles from cache to compensate for unloaded tiles:                     
444                         repopulateVisibleTiles(minCol, maxCol, minRow, maxRow);
445
446                         // move visible tiles to the end of recentlySeen if we're done loading them
447                         // the 'least recently seen' tiles will be removed from the tileCache below
448                         for each (var visibleTile:Tile in visibleTiles) {
449                                 if (!layersNeeded[visibleTile.name]) {
450                                         var ri:int = recentlySeen.indexOf(visibleTile.name);
451                                         if (ri >= 0) {
452                                                 recentlySeen.splice(ri, 1);
453                                         }
454                                         recentlySeen.push(visibleTile.name);
455                                 }
456                         }
457
458                         // prune tiles from the well if they shouldn't be there (not currently in visibleTiles)
459                         // TODO: unless they're fading in or out?
460                         // (loop backwards so removal doesn't change i)
461                         for (var i:int = well.numChildren-1; i >= 0; i--) {
462                                 var wellTile:Tile = well.getChildAt(i) as Tile;
463                                 if (visibleTiles.indexOf(wellTile) < 0) {
464                                         well.removeChild(wellTile);
465                                         wellTile.hide();
466                                         if (!tileCache.containsKey(wellTile.name)) {
467                                                 //trace("destroying tile that was in the well but never cached");
468                                                 delete layersNeeded[wellTile.name];
469                                                 if (tileQueue.contains(wellTile)) {
470                                                         tileQueue.remove(wellTile);
471                                                 }
472                                                 tilePool.returnTile(wellTile);
473                                         }
474                                 }
475                         }
476
477                         // position tiles such that currentZoom is approximately scale 1
478                         // and x and y make sense in pixels relative to tlC.column and tlC.row (topleft)
479                         positionTiles(tlC.column, tlC.row);
480
481                         // all the visible tiles will be at the end of recentlySeen
482                         // let's make sure we keep them around:
483                         var maxRecentlySeen:int = Math.max(visibleTiles.length, maxTilesToKeep);
484                        
485                         // prune cache of already seen tiles if it's getting too big:
486                         if (recentlySeen.length > maxRecentlySeen) {
487
488                                 // can we sort so that biggest zoom levels get removed first, without removing currently visible tiles?
489 /*                              var visibleKeys:Array = recentlySeen.slice(recentlySeen.length - visibleTiles.length, recentlySeen.length);
490
491                                 // take a look at everything else
492                                 recentlySeen = recentlySeen.slice(0, recentlySeen.length - visibleTiles.length);
493                                 recentlySeen = recentlySeen.sort(Array.DESCENDING);
494                                 recentlySeen = recentlySeen.concat(visibleKeys); */
495                                
496                                 // throw away keys at the beginning of recentlySeen
497                                 recentlySeen = recentlySeen.slice(recentlySeen.length - maxRecentlySeen, recentlySeen.length);
498                                
499                                 // loop over our internal tile cache
500                                 // and throw out tiles not in recentlySeen
501                                 tileCache.retainKeys(recentlySeen);
502                         }
503                        
504                         // update centerRow and centerCol for sorting the tileQueue in processQueue()
505                         var center:Coordinate = centerCoordinate.zoomTo(currentTileZoom);
506                         centerRow = center.row;
507                         centerColumn = center.column;
508
509                         onRendered();
510
511                         dirty = false;
512                 }
513                                
514                 /**
515                  * loops over given cols and rows and adds tiles to visibleTiles array and the well
516                  * using child or parent tiles to compensate for tiles not yet available in the tileCache
517                  */
518                 private function repopulateVisibleTiles(minCol:int, maxCol:int, minRow:int, maxRow:int):void
519                 {
520                         visibleTiles = [];
521                        
522                         blankCount = 0; // keep count of how many tiles we missed?
523                
524                         // for use in loops etc.
525                         var coord:Coordinate = new Coordinate(0,0,0);
526
527                         var searchedParentKeys:Object = {};
528                
529                         // loop over currently visible tiles
530                         for (var col:int = minCol; col <= maxCol; col++) {
531                                 for (var row:int = minRow; row <= maxRow; row++) {
532                                        
533                                         // create a string key for this tile
534                                         var key:String = tileKey(col, row, currentTileZoom);
535                                        
536                                         // see if we already have this tile
537                                         var tile:Tile = well.getChildByName(key) as Tile;
538                                                                                
539                                         // create it if not, and add it to the load queue
540                                         if (!tile) {
541                                                 tile = tileCache.getTile(key);
542                                                 if (!tile) {
543                                                         tile = tilePool.getTile(col, row, currentTileZoom);
544                                                         tile.name = key;
545                                                         coord.row = tile.row;
546                                                         coord.column = tile.column;
547                                                         coord.zoom = tile.zoom;
548                                                         var urls:Array = provider.getTileUrls(coord);
549                                                         if (urls && urls.length > 0) {
550                                                                 // keep a local copy of the URLs so we don't have to call this twice:
551                                                                 layersNeeded[tile.name] = urls;
552                                                                 tileQueue.push(tile);
553                                                         }
554                                                         else {
555                                                                 tile.show();
556                                                         }
557                                                 }
558                                                 else {
559                                                         tile.show();
560                                                 }
561                                                 well.addChild(tile);
562                                         }
563                                        
564                                         visibleTiles.push(tile);
565                                        
566                                         var tileReady:Boolean = tile.isShowing() && (layersNeeded[tile.name] == null);
567                                        
568                                         //
569                                         // if the tile isn't ready yet, we're going to reuse a parent tile
570                                         // if there isn't a parent tile, and we're zooming out, we'll reuse child tiles
571                                         // if we don't get all 4 child tiles, we'll look at more parent levels
572                                         //
573                                         // yes, this is quite involved, but it should be fast enough because most of the loops
574                                         // don't get hit most of the time
575                                         //
576                                        
577                                         if (!tileReady) {
578                                        
579                                                 var foundParent:Boolean = false;
580                                                 var foundChildren:int = 0;
581        
582                                                 if (currentTileZoom > previousTileZoom) {
583                                                        
584                                                         // if it still doesn't have enough images yet, or it's fading in, try a double size parent instead
585                                                         if (maxParentSearch > 0 && currentTileZoom > minZoom) {
586                                                                 var firstParentKey:String = parentKey(col, row, currentTileZoom, currentTileZoom-1);
587                                                                 if (!searchedParentKeys[firstParentKey]) {
588                                                                         searchedParentKeys[firstParentKey] = true;
589                                                                         if (ensureVisible(firstParentKey)) {
590                                                                                 foundParent = true;
591                                                                         }
592                                                                         if (!foundParent && (currentTileZoom - 1 < maxParentLoad)) {
593                                                                                 //trace("requesting parent tile at zoom", pzoom);
594                                                                                 var firstParentCoord:Array = parentCoord(col, row, currentTileZoom, currentTileZoom-1);
595                                                                                 visibleTiles.push(requestLoad(firstParentCoord[0], firstParentCoord[1], currentTileZoom-1));
596                                                                         }                                                                       
597                                                                 }
598                                                         }
599                                                        
600                                                 }
601                                                 else {
602                                                          
603                                                         // currentZoom <= previousZoom, so we're zooming out
604                                                         // and therefore we might want to reuse 'smaller' tiles
605                                                        
606                                                         // if it doesn't have an image yet, see if we can make it from smaller images
607                                                         if (!foundParent && maxChildSearch > 0 && currentTileZoom < maxZoom) {
608                                                                 for (var czoom:int = currentTileZoom+1; czoom <= Math.min(maxZoom, currentTileZoom+maxChildSearch); czoom++) {
609                                                                         var ckeys:Array = childKeys(col, row, currentTileZoom, czoom);
610                                                                         for each (var ckey:String in ckeys) {
611                                                                                 if (ensureVisible(ckey)) {
612                                                                                         foundChildren++;
613                                                                                 }
614                                                                         } // ckeys
615                                                                         if (foundChildren == ckeys.length) {
616                                                                                 break;
617                                                                         }
618                                                                 } // czoom
619                                                         }
620                                                 }
621        
622                                                 var stillNeedsAnImage:Boolean = !foundParent && foundChildren < 4;                                     
623        
624                                                 // if it still doesn't have an image yet, try more parent zooms
625                                                 if (stillNeedsAnImage && maxParentSearch > 1 && currentTileZoom > minZoom) {
626
627                                                         var startZoomSearch:int = currentTileZoom - 1;
628                                                        
629                                                         if (currentTileZoom > previousTileZoom) {
630                                                                 // we already looked for parent level 1, and didn't find it, so:
631                                                                 startZoomSearch -= 1;
632                                                         }
633                                                        
634                                                         var endZoomSearch:int = Math.max(minZoom, currentTileZoom-maxParentSearch);
635                                                        
636                                                         for (var pzoom:int = startZoomSearch; pzoom >= endZoomSearch; pzoom--) {
637                                                                 var pkey:String = parentKey(col, row, currentTileZoom, pzoom);
638                                                                 if (!searchedParentKeys[pkey]) {
639                                                                         searchedParentKeys[pkey] = true;
640                                                                         if (ensureVisible(pkey)) {                                                             
641                                                                                 stillNeedsAnImage = false;
642                                                                                 break;
643                                                                         }
644                                                                         if (currentTileZoom - pzoom < maxParentLoad) {
645                                                                                 //trace("requesting parent tile at zoom", pzoom);
646                                                                                 var pcoord:Array = parentCoord(col, row, currentTileZoom, pzoom);
647                                                                                 visibleTiles.push(requestLoad(pcoord[0], pcoord[1], pzoom));
648                                                                         }
649                                                                 }
650                                                                 else {
651                                                                         break;
652                                                                 }
653                                                         }
654                                                        
655                                                 }
656                                                                                        
657                                                 if (stillNeedsAnImage) {
658                                                         blankCount++;
659                                                 }
660
661                                         } // if !tileReady
662                                        
663                                 } // for row
664                         } // for col
665                        
666                         // trace("zoomLevel", zoomLevel, "currentTileZoom", currentTileZoom, "blankCount", blankCount);
667                        
668                 } // repopulateVisibleTiles
669                
670                 private function positionTiles(realMinCol:Number, realMinRow:Number):void
671                 {
672                         // sort children by difference from current zoom level
673                         // this means current is on top, +1 and -1 are next, then +2 and -2, etc.
674                         visibleTiles.sort(distanceFromCurrentZoomCompare, Array.DESCENDING);
675                        
676 /*                      var zooms:Array = visibleTiles.map(function(tile:Tile, ...rest):int {
677                                 return tile.zoom;
678                         });
679                         trace("currentTileZoom", currentTileZoom);
680                         trace("tile zooms:", zooms); */
681
682                         // for fixing positions when we're between zoom levels:
683                         var positionScaleCompensation:Number = Math.pow(2, zoomLevel-currentTileZoom);
684                        
685                         // for positioning tile according to current transform, based on current tile zoom
686                         var scaleFactors:Array = new Array(maxZoom+1);
687                         // scales to compensate for zoom differences between current grid zoom level                           
688                         var tileScales:Array = new Array(maxZoom+1);
689                         for (var z:int = 0; z <= maxZoom; z++) {
690                                 scaleFactors[z] = Math.pow(2.0, currentTileZoom-z)
691                                
692                                 // round up to the nearest pixel to avoid seams between zoom levels
693                                 if (roundScalesEnabled) {
694                                         tileScales[z] = Math.ceil(Math.pow(2, zoomLevel-z) * tileWidth) / tileWidth;
695                                 }
696                                 else {
697                                         tileScales[z] = Math.pow(2, zoomLevel-z);
698                                 }
699                         }
700                        
701                         //trace();
702                         //trace("tile.zoom, tile.alpha, tile.numChildren ? tile.getChildAt(0).alpha : '', tile.isShowing() && (layersNeeded[tile.name] == null), tileCache.containsKey(tile.name)");
703                        
704                         // apply the sorted depths, position all the tiles and also keep recentlySeen updated:
705                         for each (var tile:Tile in visibleTiles) {
706                        
707                                 // if we set them all to numChildren-1, descending, they should end up correctly sorted
708                                 well.setChildIndex(tile, well.numChildren-1);
709
710                                 var positionCol:Number = (scaleFactors[tile.zoom]*tile.column) - realMinCol;
711                                 var positionRow:Number = (scaleFactors[tile.zoom]*tile.row) - realMinRow;
712
713                                 tile.scaleX = tile.scaleY = tileScales[tile.zoom];
714
715                                 if (!zooming && roundPositionsEnabled) {
716                                         // this also helps the rare seams not fixed by rounding the tile scale,
717                                         // but makes slow zooming uglier:
718                                         // round, not floor, because the latter causes artifacts at lower zoom levels :(
719                                         tile.x = Math.round(positionCol*tileWidth*positionScaleCompensation);
720                                         tile.y = Math.round(positionRow*tileHeight*positionScaleCompensation);
721                                 }
722                                 else {
723                                         tile.x = positionCol*tileWidth*positionScaleCompensation;
724                                         tile.y = positionRow*tileHeight*positionScaleCompensation;
725                                 }
726                                
727                         }
728                 }
729                
730                 /** called by the onEnterFrame handler to manage the tileQueue
731                  *  usual operation is extremely quick, ~1ms or so */
732                 private function processQueue(event:TimerEvent=null):void
733                 {
734                         if (openRequests.length < maxOpenRequests && tileQueue.length > 0) {
735
736                                 // prune queue for tiles that aren't visible
737                                 var removedTiles:Array = tileQueue.retainAll(visibleTiles);
738                                
739                                 // keep layersNeeded tidy:
740                                 for each (var removedTile:Tile in removedTiles) {
741                                         delete layersNeeded[removedTile.name];
742                                 }
743                                
744                                 // note that queue is not the same as visible tiles, because things
745                                 // that have already been loaded are also in visible tiles. if we
746                                 // reuse visible tiles for the queue we'll be loading the same things over and over
747        
748                                 if (maxParentLoad == 0) {
749                                         // sort queue by distance from 'center'
750                                         tileQueue.sortTiles(centerDistanceCompare);
751                                 }
752                                 else {
753                                         tileQueue.sortTiles(zoomThenCenterCompare);                                     
754                                 }
755                                                                
756                                 // process the queue
757                                 while (openRequests.length < maxOpenRequests && tileQueue.length > 0) {
758                                         var tile:Tile = tileQueue.shift();
759                                         // if it's still on the stage:
760                                         if (tile.parent) {
761                                                 loadNextURLForTile(tile);
762                                         }
763                                 }
764                         }
765
766                         // you might want to wait for tiles to load before displaying other data, interface elements, etc.
767                         // these events take care of that for you...
768                         if (previousOpenRequests == 0 && openRequests.length > 0) {
769                                 onBeginTileLoading();
770                         }
771                         else if (previousOpenRequests > 0)
772                         {
773                                 // TODO: a custom event for load progress rather than overloading bytesloaded?
774                                 onProgress();
775
776                             // if we're finished...
777                             if (openRequests.length == 0)
778                             {
779                                 onAllTilesLoaded();
780                                 // request redraw to take parent and child tiles off the stage if we haven't already
781                                 dirty = true;
782                         }
783                         }
784                        
785                         previousOpenRequests = openRequests.length;
786                 }
787
788                 private function loadNextURLForTile(tile:Tile):void
789                 {
790                         // TODO: add urls to Tile?
791                         var urls:Array = layersNeeded[tile.name] as Array;
792                         if (urls && urls.length > 0) {
793                                 var url:* = urls.shift();
794                                 if (cacheLoaders && (url is String) && loaderCache[url]) {
795                                         var original:Bitmap = loaderCache[url] as Bitmap;
796                                         var bitmap:Bitmap = new Bitmap(original.bitmapData);
797                                         tile.addChild(bitmap);
798                                         loadNextURLForTile(tile);
799                                 }
800                                 else {
801                                         var tileLoader:Loader = new Loader();
802                                         tileLoader.name = tile.name;
803                                         try {
804                                                 if (cacheLoaders || smoothContent) {
805                                                         // check crossdomain permissions on tiles if we plan to access their bitmap content
806                                                         tileLoader.load((url is URLRequest) ? url : new URLRequest(url), new LoaderContext(true));
807                                                 }
808                                                 else {
809                                                         tileLoader.load((url is URLRequest) ? url : new URLRequest(url));
810                                                 }
811                                                 tileLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoadEnd, false, 0, true);
812                                                 tileLoader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, onLoadError, false, 0, true);
813                                                 openRequests.push(tileLoader);
814                                         }
815                                         catch(error:Error) {
816                                                 tile.paintError();
817                                         }
818                                 }
819                         }
820                         else if (urls && urls.length == 0) {
821                                 if (currentTileZoom-tile.zoom <= maxParentLoad) {
822                                         tile.show();
823                                 }
824                                 else {
825                                         tile.showNow();
826                                 }
827                                 tileCache.putTile(tile);                                       
828                                 delete layersNeeded[tile.name];
829                         }                       
830                 }
831
832                 private function zoomThenCenterCompare(t1:Tile, t2:Tile):int
833                 {
834                         if (t1.zoom == t2.zoom) {
835                                 return centerDistanceCompare(t1, t2);
836                         }
837                         return t1.zoom < t2.zoom ? -1 : t1.zoom > t2.zoom ? 1 : 0;
838                 }
839
840                 // for sorting arrays of tiles by distance from center Coordinate               
841                 private function centerDistanceCompare(t1:Tile, t2:Tile):int
842                 {
843                         if (t1.zoom == t2.zoom && t1.zoom == currentTileZoom && t2.zoom == currentTileZoom) {
844                                 var d1:int = Math.pow(t1.row+0.5-centerRow,2) + Math.pow(t1.column+0.5-centerColumn,2);
845                                 var d2:int = Math.pow(t2.row+0.5-centerRow,2) + Math.pow(t2.column+0.5-centerColumn,2);
846                                 return d1 < d2 ? -1 : d1 > d2 ? 1 : 0;
847                         }
848                         return Math.abs(t1.zoom-currentTileZoom) < Math.abs(t2.zoom-currentTileZoom) ? -1 : 1;
849                 }
850                
851                 // for sorting arrays of tiles by distance from currentZoom             
852                 private function distanceFromCurrentZoomCompare(t1:Tile, t2:Tile):int
853                 {
854                         var d1:int = Math.abs(t1.zoom-currentTileZoom);
855                         var d2:int = Math.abs(t2.zoom-currentTileZoom);
856                         return d1 < d2 ? -1 : d1 > d2 ? 1 : zoomCompare(t2, t1); // t2, t1 so that big tiles are on top of small
857                 }
858
859                 // for when tiles have same difference in zoom in distanceFromCurrentZoomCompare               
860                 private static function zoomCompare(t1:Tile, t2:Tile):int
861                 {
862                         return t1.zoom == t2.zoom ? 0 : t1.zoom > t2.zoom ? 1 : -1;
863                 }
864
865                 // makes sure that if a tile with the given key exists in the cache that it is added to the well and added to visibleTiles
866                 // returns null if tile does not exist in cache
867                 private function ensureVisible(key:String):Tile
868                 {
869                         if (tileCache.containsKey(key)) {
870                                 var tile:Tile = well.getChildByName(key) as Tile;
871                                 if (!tile) {
872                                         tile = tileCache.getTile(key);
873                                         well.addChildAt(tile,0);
874                                         if (currentTileZoom-tile.zoom <= maxParentLoad) {
875                                                 tile.show();
876                                         }
877                                         else {
878                                                 tile.showNow();                                         
879                                         }
880                                 }
881                                 if (visibleTiles.indexOf(tile) < 0) {
882                                         visibleTiles.push(tile); // don't get rid of it yet!
883                                 }
884                                 return tile;
885                         }
886                         return null;
887                 }
888                
889                 // for use in requestLoad
890                 private var tempCoord:Coordinate = new Coordinate(0,0,0);
891
892                 /** create a tile and add it to the queue - WARNING: this is buggy for the current zoom level, it's only used for parent zooms when maxParentLoad is > 0 */
893                 private function requestLoad(col:int, row:int, zoom:int):Tile
894                 {
895                         var key:String = tileKey(col, row, zoom);
896                         if (tileCache.containsKey(key)) throw new Error("requested load for an already cached tile");                   
897                         var tile:Tile = well.getChildByName(key) as Tile;
898                         if (!tile) {
899                                 tile = tilePool.getTile(col, row, zoom);
900                                 tile.name = key;
901                                 tempCoord.row = row;
902                                 tempCoord.column = col;
903                                 tempCoord.zoom = zoom;
904                                 var urls:Array = provider.getTileUrls(tempCoord);
905                                 if (urls && urls.length > 0) {
906                                         // keep a local copy of the URLs so we don't have to call this twice:
907                                         layersNeeded[tile.name] = urls;
908                                         tileQueue.push(tile);
909                                 }
910                                 else {
911                                         // trace("no urls needed for that tile", tempCoord);
912                                         tile.show();
913                                 }
914                                 well.addChild(tile);
915                         }
916                         return tile;
917                 }
918
919                 private static const zoomLetter:Array = "abcdefghijklmnopqrstuvwxyz".split('');
920                                                
921                 /** zoom is translated into a letter so that keys can easily be sorted (alphanumerically) by zoom level */
922                 private function tileKey(col:int, row:int, zoom:int):String
923                 {
924                         return zoomLetter[zoom]+":"+col+":"+row;
925                 }
926                
927                 // TODO: check that this does the right thing with negative row/col?
928                 private function parentKey(col:int, row:int, zoom:int, parentZoom:int):String
929                 {
930                         var scaleFactor:Number = Math.pow(2.0, zoom-parentZoom);
931                         var pcol:int = Math.floor(Number(col) / scaleFactor);
932                         var prow:int = Math.floor(Number(row) / scaleFactor);
933                         return tileKey(pcol,prow,parentZoom);                   
934                 }
935
936                 // used when maxParentLoad is > 0
937                 // TODO: check that this does the right thing with negative row/col?
938                 private function parentCoord(col:int, row:int, zoom:int, parentZoom:int):Array
939                 {
940                         var scaleFactor:Number = Math.pow(2.0, zoom-parentZoom);
941                         var pcol:int = Math.floor(Number(col) / scaleFactor);
942                         var prow:int = Math.floor(Number(row) / scaleFactor);
943                         return [ pcol, prow ];                 
944                 }               
945                
946                 // TODO: check that this does the right thing with negative row/col?
947                 private function childKeys(col:int, row:int, zoom:int, childZoom:int):Array
948                 {
949                         var scaleFactor:Number = Math.pow(2, zoom-childZoom); // one zoom in = 0.5
950                         var rowColSpan:int = Math.pow(2, childZoom-zoom); // one zoom in = 2, two = 4
951                         var keys:Array = [];
952                         for (var ccol:int = col/scaleFactor; ccol < (col/scaleFactor)+rowColSpan; ccol++) {
953                                 for (var crow:int = row/scaleFactor; crow < (row/scaleFactor)+rowColSpan; crow++) {
954                                         keys.push(tileKey(ccol, crow, childZoom));
955                                 }
956                         }
957                         return keys;
958                 }
959                                                
960                 private function onLoadEnd(event:Event):void
961                 {
962                         var loader:Loader = (event.target as LoaderInfo).loader;
963                        
964                         if (cacheLoaders && !loaderCache[loader.contentLoaderInfo.url]) {
965                                 //trace('caching content for', loader.contentLoaderInfo.url);
966                                 try {
967                                         var content:Bitmap = loader.content as Bitmap;
968                                         loaderCache[loader.contentLoaderInfo.url] = content;
969                                         cachedUrls.push(loader.contentLoaderInfo.url);
970                                         if (cachedUrls.length > maxLoaderCacheSize) {
971                                                 delete loaderCache[cachedUrls.shift()];
972                                         }
973                                 }
974                                 catch (error:Error) {
975                                         // ???
976                                 }
977                         }
978                        
979                         if (smoothContent) {
980                                 try {
981                                         var smoothContent:Bitmap = loader.content as Bitmap;
982                                         smoothContent.smoothing = true;
983                                 }
984                                 catch (error:Error) {
985                                         // ???
986                                 }
987                         }                       
988
989                         // tidy up the request monitor
990                         var index:int = openRequests.indexOf(loader);
991                         if (index >= 0) {
992                                 openRequests.splice(index,1);
993                         }
994                        
995                         var tile:Tile = well.getChildByName(loader.name) as Tile;
996                         if (tile) {
997                                 tile.addChild(loader);
998                                 loadNextURLForTile(tile);
999                         }
1000                         else {
1001                                 // we've loaded an image, but its parent tile has been removed
1002                                 // so we'll have to throw it away
1003                         }
1004                 }
1005
1006                 private function onLoadError(event:IOErrorEvent):void
1007                 {
1008                         var loaderInfo:LoaderInfo = event.target as LoaderInfo;
1009                         for (var i:int = openRequests.length-1; i >= 0; i--) {
1010                                 var loader:Loader = openRequests[i] as Loader;
1011                                 if (loader.contentLoaderInfo == loaderInfo) {
1012                                         openRequests.splice(i,1);
1013                                         var tile:Tile = well.getChildByName(loader.name) as Tile;
1014                                         if (tile) {
1015                                                 delete layersNeeded[tile.name];
1016                                                 tile.paintError(tileWidth, tileHeight);
1017                                                 if (currentTileZoom-tile.zoom <= maxParentLoad) {
1018                                                         tile.show();
1019                                                 }
1020                                                 else {
1021                                                         tile.showNow();
1022                                                 }
1023                                         }
1024                                         break;
1025                                 }
1026                         }
1027                 }       
1028                
1029                 public function mousePressed(event:MouseEvent):void
1030                 {
1031                         prepareForPanning(true);
1032                         pmouse = new Point(event.stageX, event.stageY);
1033                         stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseDragged);
1034                         stage.addEventListener(MouseEvent.MOUSE_UP, mouseReleased);
1035                         stage.addEventListener(Event.MOUSE_LEAVE, mouseReleased);
1036                 }
1037
1038                 public function mouseReleased(event:Event):void
1039                 {
1040                         stage.removeEventListener(MouseEvent.MOUSE_MOVE, mouseDragged);
1041                         stage.removeEventListener(MouseEvent.MOUSE_UP, mouseReleased);
1042                         stage.removeEventListener(Event.MOUSE_LEAVE, mouseReleased);
1043                         donePanning();
1044                         dirty = true;
1045                         if (event is MouseEvent) {
1046                                 MouseEvent(event).updateAfterEvent();
1047                         }
1048                         else if (event.type == Event.MOUSE_LEAVE) {
1049                                 onRender();
1050                         }
1051                 }
1052
1053                 public function mouseDragged(event:MouseEvent):void
1054                 {
1055                         var mousePoint:Point = new Point(event.stageX, event.stageY);
1056                         tx += mousePoint.x - pmouse.x;
1057                         ty += mousePoint.y - pmouse.y;
1058                         pmouse = mousePoint;
1059                         dirty = true;
1060                         event.updateAfterEvent();
1061                 }       
1062
1063                 // today is all about lazy evaluation
1064                 // this gets set to null by 'dirty = true'
1065                 // and only calculated again if you need it
1066                 protected function get invertedMatrix():Matrix
1067                 {
1068                         if (!_invertedMatrix) {
1069                                 _invertedMatrix = worldMatrix.clone();
1070                                 _invertedMatrix.invert();
1071                                 _invertedMatrix.scale(scale/tileWidth, scale/tileHeight);
1072                         }
1073                         return _invertedMatrix;
1074                 }
1075
1076                 /** derived from map provider by calculateBounds(), read-only here for convenience */
1077                 public function get minZoom():Number
1078                 {
1079                         return _minZoom;
1080                 }
1081                 /** derived from map provider by calculateBounds(), read-only here for convenience */
1082                 public function get maxZoom():Number
1083                 {
1084                         return _maxZoom;
1085                 }
1086
1087                 /** convenience method for tileWidth */
1088                 public function get tileWidth():Number
1089                 {
1090                         return _tileWidth;
1091                 }
1092                 /** convenience method for tileHeight */
1093                 public function get tileHeight():Number
1094                 {
1095                         return _tileHeight;
1096                 }
1097
1098                 /** read-only, this is the level of tiles we'll be loading first */
1099                 public function get currentTileZoom():Number
1100                 {
1101                         return _currentTileZoom;
1102                 }
1103
1104
1105                 public function get topLeftCoordinate():Coordinate
1106                 {
1107                         if (!_topLeftCoordinate) {
1108                                 var tl:Point = invertedMatrix.transformPoint(new Point());
1109                                 _topLeftCoordinate = new Coordinate(tl.y, tl.x, zoomLevel);                     
1110                         }
1111                         return _topLeftCoordinate;
1112                 }
1113
1114                 public function get bottomRightCoordinate():Coordinate
1115                 {
1116                         if (!_bottomRightCoordinate) {
1117                                 var br:Point = invertedMatrix.transformPoint(new Point(mapWidth, mapHeight));
1118                                 _bottomRightCoordinate = new Coordinate(br.y, br.x, zoomLevel);                 
1119                         }
1120                         return _bottomRightCoordinate;
1121                 }
1122                                                
1123                 public function get centerCoordinate():Coordinate
1124                 {
1125                         if (!_centerCoordinate) {
1126                                 var c:Point = invertedMatrix.transformPoint(new Point(mapWidth/2, mapHeight/2));
1127                                 _centerCoordinate = new Coordinate(c.y, c.x, zoomLevel);
1128                         }
1129                         return _centerCoordinate;                       
1130                 }
1131                
1132                 public function coordinatePoint(coord:Coordinate, context:DisplayObject=null):Point
1133                 {
1134                         // this is the same as coord.zoomTo, but doesn't make a new Coordinate:
1135                         var zoomFactor:Number = Math.pow(2, zoomLevel - coord.zoom);
1136                         //zoomFactor *= tileWidth/scale;
1137                         var zoomedColumn:Number = coord.column * zoomFactor;
1138                         var zoomedRow:Number = coord.row * zoomFactor;
1139                        
1140                         var tl:Coordinate = topLeftCoordinate;
1141                         var br:Coordinate = bottomRightCoordinate;
1142                        
1143                         var cols:Number = br.column - tl.column;
1144                         var rows:Number = br.row - tl.row;
1145                        
1146                         var screenPoint:Point = new Point(mapWidth * (zoomedColumn-tl.column) / cols, mapHeight * (zoomedRow-tl.row) / rows);
1147                        
1148                         //var screenPoint:Point = worldMatrix.transformPoint(new Point(zoomedColumn, zoomedRow));
1149
1150                         if (context && context != this)
1151             {
1152                         screenPoint = this.parent.localToGlobal(screenPoint);
1153                         screenPoint = context.globalToLocal(screenPoint);
1154             }
1155
1156                         return screenPoint;
1157                 }
1158                 public function pointCoordinate(point:Point, context:DisplayObject=null):Coordinate
1159                 {                       
1160                         if (context && context != this)
1161             {
1162                         point = context.localToGlobal(point);
1163                         point = this.globalToLocal(point);
1164             }
1165                        
1166                         var p:Point = invertedMatrix.transformPoint(point);
1167                         return new Coordinate(p.y, p.x, zoomLevel);
1168                 }
1169                
1170                 public function prepareForPanning(dragging:Boolean=false):void
1171                 {
1172                         if (panning) {
1173                                 donePanning();
1174                         }
1175                         if (!dragging && draggable) {
1176                                 if (hasEventListener(MouseEvent.MOUSE_DOWN)) {
1177                                         removeEventListener(MouseEvent.MOUSE_DOWN, mousePressed, true);
1178                                 }
1179                         }
1180                         startPan = centerCoordinate.copy();
1181                         panning = true;
1182                         onStartPanning();
1183                 }
1184                
1185                 protected function onStartPanning():void
1186                 {
1187                         dispatchEvent(new MapEvent(MapEvent.START_PANNING));
1188                 }
1189                
1190                 public function donePanning():void
1191                 {
1192                         if (draggable) {
1193                                 if (!hasEventListener(MouseEvent.MOUSE_DOWN)) {
1194                                         addEventListener(MouseEvent.MOUSE_DOWN, mousePressed, true);
1195                                 }
1196                         }
1197                         startPan = null;
1198                         panning = false;
1199                         onStopPanning();
1200                 }
1201                
1202                 protected function onStopPanning():void
1203                 {
1204                         dispatchEvent(new MapEvent(MapEvent.STOP_PANNING));
1205                 }
1206                
1207                 public function prepareForZooming():void
1208                 {
1209                         if (startZoom >= 0) {
1210                                 doneZooming();
1211                         }
1212
1213                         startZoom = zoomLevel;
1214                         zooming = true;
1215                         onStartZooming();
1216                 }
1217                
1218                 protected function onStartZooming():void
1219                 {
1220                         dispatchEvent(new MapEvent(MapEvent.START_ZOOMING, startZoom));
1221                 }
1222                                        
1223                 public function doneZooming():void
1224                 {
1225                         onStopZooming();
1226                         startZoom = -1;
1227                         zooming = false;
1228                 }
1229
1230                 protected function onStopZooming():void
1231                 {
1232                     var event:MapEvent = new MapEvent(MapEvent.STOP_ZOOMING, zoomLevel);
1233                     event.zoomDelta = zoomLevel - startZoom;
1234                         dispatchEvent(event);
1235                 }
1236
1237                 public function resetTiles(coord:Coordinate):void
1238                 {
1239                         var sc:Number = Math.pow(2, coord.zoom);
1240
1241                         worldMatrix.identity();
1242                         worldMatrix.scale(sc, sc);
1243                         worldMatrix.translate(mapWidth/2, mapHeight/2 );
1244                         worldMatrix.translate(-tileWidth*coord.column, -tileHeight*coord.row);
1245
1246                         // reset the inverted matrix, request a redraw, etc.
1247                         dirty = true;
1248                 }
1249
1250                 public function get zoomLevel():Number
1251                 {
1252                         return Math.log(scale) / Math.LN2;
1253                 }
1254
1255                 public function set zoomLevel(n:Number):void
1256                 {
1257                     if (zoomLevel != n)
1258                     {
1259                         scale = Math.pow(2, n);                                         
1260             }
1261                 }
1262
1263                 public function get scale():Number
1264                 {
1265                         return Math.sqrt(worldMatrix.a * worldMatrix.a + worldMatrix.b * worldMatrix.b);
1266                 }
1267
1268                 public function set scale(n:Number):void
1269                 {
1270                     if (scale != n)
1271                     {
1272                         var needsStop:Boolean = false;
1273                         if (!zooming) {
1274                                 prepareForZooming();
1275                                 needsStop = true;
1276                         }
1277                        
1278                         var sc:Number = n / scale;
1279                         worldMatrix.translate(-mapWidth/2, -mapHeight/2);
1280                         worldMatrix.scale(sc, sc);
1281                         worldMatrix.translate(mapWidth/2, mapHeight/2);
1282                        
1283                         dirty = true;   
1284                        
1285                         if (needsStop) {
1286                                 doneZooming();
1287                         }
1288                     }
1289                 }
1290                                
1291                 public function resizeTo(p:Point):void
1292                 {
1293                     if (mapWidth != p.x || mapHeight != p.y)
1294                     {
1295                         var dx:Number = p.x - mapWidth;
1296                         var dy:Number = p.y - mapHeight;
1297                        
1298                         // maintain the center point:
1299                         tx += dx/2;
1300                         ty += dy/2;
1301                        
1302                         mapWidth = p.x;
1303                         mapHeight = p.y;
1304                 scrollRect = new Rectangle(0, 0, mapWidth, mapHeight);
1305
1306                                 debugField.x = mapWidth - debugField.width - 15;
1307                                 debugField.y = mapHeight - debugField.height - 15;
1308                        
1309                         dirty = true;
1310
1311                         // force this but only for onResize
1312                         onRender();
1313                     }
1314
1315                         // this makes sure the well is clickable even without tiles
1316                         well.graphics.clear();
1317                         well.graphics.beginFill(0x000000, 0);
1318                         well.graphics.drawRect(0, 0, mapWidth, mapHeight);
1319                         well.graphics.endFill();
1320                 }
1321                
1322                 public function setMapProvider(provider:IMapProvider):void
1323                 {
1324                         this.provider = provider;
1325
1326                         _tileWidth = provider.tileWidth;
1327                         _tileHeight = provider.tileHeight;
1328                        
1329                         calculateBounds();
1330                        
1331                         clearEverything();
1332                 }
1333                
1334                 protected function clearEverything():void
1335                 {
1336                         while (well.numChildren > 0) {                 
1337                                 var tile:Tile = well.removeChildAt(0) as Tile;
1338                                 if (!tileCache.containsKey(tile.name)) {
1339                                         delete layersNeeded[tile.name];
1340                                         tilePool.returnTile(tile);
1341                                 }
1342                         }
1343                        
1344                         for each (var loader:Loader in openRequests) {
1345                                 try {
1346                                         // la la I can't hear you
1347                                         loader.contentLoaderInfo.removeEventListener(Event.COMPLETE, onLoadEnd);
1348                                         loader.contentLoaderInfo.removeEventListener(IOErrorEvent.IO_ERROR, onLoadError);
1349                                         loader.close();
1350                                 }
1351                                 catch (error:Error) {
1352                                         // close often doesn't work, no biggie
1353                                 }
1354                         }
1355                         openRequests = [];
1356                        
1357                         for (var key:String in layersNeeded) {
1358                                 delete layersNeeded[key];
1359                         }
1360                         layersNeeded = {};
1361
1362                         recentlySeen = [];
1363                        
1364                         tileQueue.clear();                     
1365                         tileCache.clear();
1366                        
1367                         dirty = true;
1368                 }
1369
1370                 protected function calculateBounds():void
1371                 {
1372                         var limits:Array = provider.outerLimits();                     
1373                         var tl:Coordinate = limits[0] as Coordinate;
1374                         var br:Coordinate = limits[1] as Coordinate;
1375
1376                         _maxZoom = Math.max(tl.zoom, br.zoom); 
1377                         _minZoom = Math.min(tl.zoom, br.zoom);
1378                        
1379                         tl = tl.zoomTo(0);
1380                         br = br.zoomTo(0);
1381
1382                         minTx = tl.column * tileWidth;
1383                         maxTx = br.column * tileWidth;
1384                         minTy = tl.row * tileHeight;
1385                         maxTy = br.row * tileHeight;
1386                 }
1387                
1388                 /** called inside of onRender before events are fired
1389                  *  don't use setters inside of here to correct values otherwise we'll get stuck in a loop! */
1390                 protected function enforceBounds():Boolean
1391                 {
1392                         if (!enforceBoundsEnabled) {
1393                                 return false;
1394                         }
1395                        
1396                         // TODO: should this modify things directly and return true?
1397                         if (zoomLevel < minZoom) {
1398                                 scale = Math.pow(2, minZoom);
1399                         }
1400                         else if (zoomLevel > maxZoom) {
1401                                 scale = Math.pow(2, maxZoom);
1402                         }
1403
1404                         var touched:Boolean = false;
1405
1406 /*                      this is potentially the way to wrap the x position
1407                         but all the tiles flash and the values aren't quite right
1408                         so wrapping the matrix needs more work :(
1409                        
1410                         var wrapTx:Number = 256 * scale;
1411                        
1412                         if (worldMatrix.tx > 0) {
1413                                 worldMatrix.tx = worldMatrix.tx - wrapTx;
1414                         }
1415                         else if (worldMatrix.tx < -wrapTx) {
1416                                 worldMatrix.tx += wrapTx;
1417                         } */
1418
1419                         // to make sure we haven't gone too far
1420                         // zoom topLeft and bottomRight coords to 0
1421                         // so that they can be compared against minTx etc.
1422                        
1423                         var tl:Coordinate = topLeftCoordinate.zoomTo(0);
1424                         var br:Coordinate = bottomRightCoordinate.zoomTo(0);
1425                        
1426                         var leftX:Number = tl.column * tileWidth;
1427                         var rightX:Number = br.column * tileHeight;
1428                        
1429                         if (rightX-leftX > maxTx-minTx) {
1430                                 worldMatrix.tx = (mapWidth-(minTx+maxTx)*scale)/2;
1431                                 touched = true;
1432                         }
1433                         else if (leftX < minTx) {
1434                                 worldMatrix.tx += (leftX-minTx)*scale;                         
1435                                 touched = true;
1436                         }
1437                         else if (rightX > maxTx) {
1438                                 worldMatrix.tx += (rightX-maxTx)*scale;                         
1439                                 touched = true;
1440                         }
1441
1442                         var upY:Number = tl.row * tileHeight;
1443                         var downY:Number = br.row * tileWidth;
1444
1445                         if (downY-upY > maxTy-minTy) {
1446                                 worldMatrix.ty = (mapHeight-(minTy+maxTy)*scale)/2;
1447                                 touched = true;
1448                         }
1449                         else if (upY < minTy) {
1450                                 worldMatrix.ty += (upY-minTy)*scale;
1451                                 touched = true;
1452                         }
1453                         else if (downY > maxTy) {
1454                                 worldMatrix.ty += (downY-maxTy)*scale;
1455                                 touched = true;
1456                         }
1457
1458                         if (touched) {
1459                                 _invertedMatrix = null;
1460                                 _topLeftCoordinate = null;
1461                                 _bottomRightCoordinate = null;
1462                                 _centerCoordinate = null;                               
1463                         }
1464
1465                         return touched;                 
1466                 }
1467                
1468                 protected function set dirty(d:Boolean):void
1469                 {
1470                         _dirty = d;
1471                         if (d) {
1472                                 if (stage) stage.invalidate();
1473                                
1474                                 _invertedMatrix = null;
1475                                 _topLeftCoordinate = null;
1476                                 _bottomRightCoordinate = null;                 
1477                                 _centerCoordinate = null;                               
1478                         }
1479                 }
1480                
1481                 protected function get dirty():Boolean
1482                 {
1483                         return _dirty;
1484                 }
1485
1486                 public function getMatrix():Matrix
1487                 {
1488                         return worldMatrix.clone();
1489                 }
1490
1491                 public function setMatrix(m:Matrix):void
1492                 {
1493                         worldMatrix = m;
1494                         matrixChanged = true;
1495                         dirty = true;
1496                 }
1497                
1498                 public function get a():Number
1499                 {
1500                         return worldMatrix.a;
1501                 }
1502                 public function get b():Number
1503                 {
1504                         return worldMatrix.b;
1505                 }
1506                 public function get c():Number
1507                 {
1508                         return worldMatrix.c;
1509                 }
1510                 public function get d():Number
1511                 {
1512                         return worldMatrix.d;
1513                 }
1514                 public function get tx():Number
1515                 {
1516                         return worldMatrix.tx;
1517                 }
1518                 public function get ty():Number
1519                 {
1520                         return worldMatrix.ty;
1521                 }
1522
1523                 public function set a(n:Number):void
1524                 {
1525                         worldMatrix.a = n;
1526                         dirty = true;
1527                 }
1528                 public function set b(n:Number):void
1529                 {
1530                         worldMatrix.b = n;
1531                         dirty = true;
1532                 }
1533                 public function set c(n:Number):void
1534                 {
1535                         worldMatrix.c = n;
1536                         dirty = true;
1537                 }
1538                 public function set d(n:Number):void
1539                 {
1540                         worldMatrix.d = n;
1541                         dirty = true;
1542                 }
1543                 public function set tx(n:Number):void
1544                 {
1545                         worldMatrix.tx = n;
1546                         dirty = true;
1547                 }
1548                 public function set ty(n:Number):void
1549                 {
1550                         worldMatrix.ty = n;
1551                         dirty = true;
1552                 }
1553                                                                
1554         }
1555        
1556 }
1557
1558 import com.modestmaps.core.Tile;
1559 import flash.utils.Dictionary;
1560 import com.modestmaps.Map;     
1561
1562 class TileQueue
1563 {
1564         // Tiles we want to load:
1565         protected var queue:Array;
1566        
1567         public function TileQueue()
1568         {
1569                 queue = [];
1570         }
1571        
1572         public function get length():Number
1573         {
1574                 return queue.length;
1575         }
1576
1577         public function contains(tile:Tile):Boolean
1578         {
1579                 return queue.indexOf(tile) >= 0;
1580         }
1581
1582         public function remove(tile:Tile):void
1583         {
1584                 var index:int = queue.indexOf(tile);
1585                 if (index >= 0) {
1586                         queue.splice(index, 1);
1587                 }
1588         }
1589        
1590         public function push(tile:Tile):void
1591         {
1592                 if (contains(tile)) {
1593                         throw new Error("that tile is already on the queue!");
1594                 }
1595                 queue.push(tile);
1596         }
1597        
1598         public function shift():Tile
1599         {
1600                 return queue.shift() as Tile;
1601         }
1602        
1603         public function sortTiles(callback:Function):void
1604         {
1605                 queue = queue.sort(callback);
1606         }
1607        
1608         public function retainAll(tiles:Array):Array
1609         {
1610                 var removed:Array = [];
1611                 for (var i:int = queue.length-1; i >= 0; i--) {
1612                         var tile:Tile = queue[i] as Tile;
1613                         if (tiles.indexOf(tile) < 0) {
1614                                 removed.push(tile);
1615                                 queue.splice(i,1);
1616                         }
1617                 }
1618                 return removed;
1619         }
1620        
1621         public function clear():void
1622         {
1623                 queue = [];
1624         }
1625        
1626 }
1627
1628 /** the alreadySeen Dictionary here will contain up to grid.maxTilesToKeep Tiles */
1629 class TileCache
1630 {
1631         // Tiles we've already seen and fully loaded, by key (.name)
1632         protected var alreadySeen:Dictionary;
1633         protected var tilePool:TilePool; // for handing tiles back!
1634        
1635         public function TileCache(tilePool:TilePool)
1636         {
1637                 this.tilePool = tilePool;
1638                 alreadySeen = new Dictionary();
1639         }
1640        
1641         public function get size():int
1642         {
1643                 var alreadySeenCount:int = 0;
1644                 for (var key:* in alreadySeen) {
1645                         alreadySeenCount++;
1646                 }
1647                 return alreadySeenCount;               
1648         }
1649        
1650         public function putTile(tile:Tile):void
1651         {
1652                 if (alreadySeen[tile.name]) {
1653                         throw new Error("caching a tile that's already cached");
1654                 }
1655                 if (tile.numChildren == 0) {
1656                         throw new Error("tile added to cache has no children!");
1657                 }
1658                 alreadySeen[tile.name] = tile;
1659         }
1660        
1661         public function getTile(key:String):Tile
1662         {
1663                 return alreadySeen[key] as Tile;
1664         }
1665        
1666         public function containsKey(key:String):Boolean
1667         {
1668                 return alreadySeen[key] is Tile;
1669         }
1670        
1671         public function retainKeys(keys:Array):void
1672         {
1673                 for (var key:String in alreadySeen) {
1674                         if (keys.indexOf(key) < 0) {
1675                                 tilePool.returnTile(alreadySeen[key] as Tile);
1676                                 delete alreadySeen[key];
1677                         }
1678                 }               
1679         }
1680        
1681         public function clear():void
1682         {
1683                 for (var key:String in alreadySeen) {
1684                         tilePool.returnTile(alreadySeen[key] as Tile);
1685                         delete alreadySeen[key];
1686                 }
1687                 alreadySeen = new Dictionary();         
1688         }
1689 }
1690
1691 /**
1692  *  This post http://lab.polygonal.de/2008/06/18/using-object-pools/
1693  *  suggests that using Object pools, especially for complex classes like Sprite
1694  *  is a lot faster than calling new Object().  The suggested implementation
1695  *  uses a linked list, but to get started with it here I'm using an Array. 
1696  * 
1697  *  If anyone wants to try it with a linked list and compare the times,
1698  *  it seems like it could be worth it :)
1699  */
1700 class TilePool
1701 {
1702         protected static const MIN_POOL_SIZE:int = 256;
1703         protected static const MAX_NEW_TILES:int = 256;
1704        
1705         protected var pool:Array = [];
1706         protected var tileClass:Class;
1707        
1708         public function TilePool(tileClass:Class)
1709         {
1710                 this.tileClass = tileClass;
1711         }
1712
1713         public function setTileClass(tileClass:Class):void
1714         {
1715                 this.tileClass = tileClass;
1716                 pool = [];
1717         }
1718
1719         public function getTile(column:int, row:int, zoom:int):Tile
1720         {
1721         if (pool.length < MIN_POOL_SIZE) {
1722                 while (pool.length < MAX_NEW_TILES) {
1723                         pool.push(new tileClass(0,0,0));
1724                 }
1725         }                                               
1726                 var tile:Tile = pool.pop() as Tile;
1727                 tile.init(column, row, zoom);
1728                 return tile;
1729         }
1730
1731         public function returnTile(tile:Tile):void
1732         {
1733                 tile.destroy();
1734         pool.push(tile);
1735         }
1736        
1737 }
Note: See TracBrowser for help on using the browser.