1 package net.systemeD.halcyon {
3 import flash.display.*;
5 import flash.filters.*;
7 import flash.system.LoaderContext;
8 import flash.utils.Timer;
10 public class TileSet extends Sprite {
12 public var tile_l:int;
13 public var tile_r:int;
14 public var tile_b:int;
15 public var tile_t:int;
17 private var offset_lon:Number=0;
18 private var offset_lat:Number=0;
20 private var tiles:Object={}; // key is "z,x,y"; value "true" if queued, or reference to loader object if requested
21 private var loadcount:int=0; // number of tiles fully downloaded
22 private var baseurl:String; // e.g. http://npe.openstreetmap.org/$z/$x/$y.png
23 private var scheme:String; // 900913 or microsoft
24 public var blocks:Array; // array of regexes which are verboten
26 private var count:Number=0; // counter incremented to provide a/b/c/d tile swapping
27 private static const ROUNDROBIN:RegExp =/\$\{([^}]+)\}/;
30 private const MAXTILESLOADED:uint=30;
32 private var sharpenFilter:BitmapFilter = new ConvolutionFilter(3, 3,
36 private var sharpening:Boolean = false;
37 // http://flylib.com/books/en/2.701.1.170/1/
39 public function TileSet(map:Map) {
42 map.addEventListener(MapEvent.NUDGE_BACKGROUND, nudgeHandler);
45 /** @param params Currently includes "url" and "scheme"
46 * @param update Trigger update now?
47 * @param dim Start with imagery faded?
48 * @param sharpen Start with sharpen filter applied?
50 public function init(params:Object, update:Boolean=false):void {
52 scheme =params.scheme ? params.scheme : '900913';
54 for (var tilename:String in tiles) {
55 if (tiles[tilename] is Loader) tiles[tilename].unload();
59 offset_lon=offset_lat=x=y=0;
60 while (numChildren) { removeChildAt(0); }
62 if (update) { this.update(); }
65 private function createSprites():void {
66 for (var i:uint=map.MINSCALE; i<=map.MAXSCALE; i++) {
67 this.addChild(new Sprite());
71 /** Toggle fading of imagery. */
72 public function setDimming(dim:Boolean):void {
75 /** Is imagery currently set faded? */
76 public function getDimming():Boolean {
80 /** Toggle sharpen filter. */
81 public function setSharpen(sharpen:Boolean):void {
82 var f:Array=[]; if (sharpen) { f=[sharpenFilter]; }
83 for (var i:uint=0; i<numChildren; i++) {
84 var s:Sprite=Sprite(getChildAt(i));
85 for (var j:uint=0; j<s.numChildren; j++) {
86 s.getChildAt(j).filters=f;
92 /** Is sharpen filter applied? */
93 public function getSharpen():Boolean {
97 /** Set zoom scale (no update triggerd). */
98 public function changeScale(scale:uint):void {
99 for (var i:uint=map.MINSCALE; i<=map.MAXSCALE; i++) {
100 this.getChildAt(i-map.MINSCALE).visible=(scale==i);
102 x=map.lon2coord(map.centre_lon+offset_lon)-map.lon2coord(map.centre_lon);
103 y=map.lat2coord(map.centre_lat+offset_lat)-map.lat2coord(map.centre_lat);
106 /** Update bounds of tile area, and request new tiles if needed. */
108 public function update():void {
109 if (!baseurl) { return; }
110 tile_l=lon2tile(map.edge_l-offset_lon);
111 tile_r=lon2tile(map.edge_r-offset_lon);
112 tile_t=lat2tile(map.edge_t-offset_lat);
113 tile_b=lat2tile(map.edge_b-offset_lat);
114 for (var tx:int=tile_l; tx<=tile_r; tx++) {
115 for (var ty:int=tile_t; ty<=tile_b; ty++) {
116 if (!tiles[map.scale+','+tx+','+ty]) {
117 var loader:Loader = new Loader();
118 tiles[map.scale+','+tx+','+ty]=loader;
119 loader.contentLoaderInfo.addEventListener(Event.INIT, doImgInit, false, 0, true);
120 loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, missingTileError, false, 0, true);
121 loader.contentLoaderInfo.addEventListener(HTTPStatusEvent.HTTP_STATUS, function(e:HTTPStatusEvent):void { tileLoadStatus(e,map.scale,tx,ty); }, false, 0, true);
122 loader.load(new URLRequest(tileURL(tx,ty,map.scale)),
123 new LoaderContext(true));
124 Sprite(this.getChildAt(map.scale-map.MINSCALE)).addChild(loader);
125 loader.x=map.lon2coord(tile2lon(tx));
126 loader.y=map.lat2coord(tile2lat(ty));
127 if (sharpening) { loader.filters=[sharpenFilter]; }
133 private function missingTileError(event:Event):void {
136 private function tileLoadStatus(event:HTTPStatusEvent,z:int,x:int,y:int):void {
137 if (event.status==200) return; // fine, carry on
138 if (event.status==404) return; // doesn't exist, so ignore forever
139 // Dodgy tile response - probably a 502/503 from Bing - so can be retried
140 delete tiles[z+','+x+','+y];
143 /** Tile image has been downloaded, so start displaying it. */
144 protected function doImgInit(event:Event):void {
146 if (loadcount>MAXTILESLOADED) purgeTiles();
150 protected function purgeTiles():void {
151 for (var tile:String in tiles) {
152 if (tiles[tile] is Sprite) {
153 var coords:Array=tile.split(','); var tz:uint=coords[0]; var tx:uint=coords[1]; var ty:uint=coords[1];
154 if (tz!=map.scale || tx<tile_l || tx>tile_r || ty<tile_t || ty<tile_b) {
155 if (tiles[tile].parent) tiles[tile].parent.removeChild(tiles[tile]);
166 private function tileURL(tx:int,ty:int,tz:uint):String {
168 var tmsy:int=Math.pow(2,tz)-1-ty;
169 switch (scheme.toLowerCase()) {
173 for (var zoom:uint=tz; zoom>0; zoom--) {
175 var mask:uint=1<<(zoom-1);
176 if ((tx & mask)!=0) byte++;
177 if ((ty & mask)!=0) byte+=2;
180 t=baseurl.replace('$quadkey',u); break;
183 t=baseurl.replace('$z',map.scale).replace('$x',tx).replace('$y',tmsy);
187 if (baseurl.indexOf('$x')>-1) {
188 t=baseurl.replace('$z',map.scale).replace('$x',tx).replace('$y',ty).replace('$-y',tmsy);
190 t=baseurl.replace('!',map.scale).replace('!',tx).replace('!',ty);
195 var o:Object=new Object();
196 if ((o=ROUNDROBIN.exec(t))) {
197 var prefixes:Array=o[1].split('|');
198 var p:String = prefixes[count % prefixes.length];
199 t=t.replace(ROUNDROBIN,p);
203 for each (var block:* in blocks) { if (t.match(block)) return ''; }
207 public function get url():String {
208 return baseurl ? baseurl : '';
211 /** Respond to nudge event by updating offset between imagery and map. */
212 public function nudgeHandler(event:MapEvent):void {
213 if (!baseurl) { return; }
214 this.x+=event.params.x; this.y+=event.params.y;
215 offset_lat=map.centre_lat-map.coord2lat(map.lat2coord(map.centre_lat)-this.y);
216 offset_lon=map.centre_lon-map.coord2lon(map.lon2coord(map.centre_lon)-this.x);
221 // ------------------------------------------------------------------
222 // Co-ordinate conversion functions
224 private function lon2tile(lon:Number):int {
225 return (Math.floor((lon+180)/360*Math.pow(2,map.scale)));
227 private function lat2tile(lat:Number):int {
228 return (Math.floor((1-Math.log(Math.tan(lat*Math.PI/180) + 1/Math.cos(lat*Math.PI/180))/Math.PI)/2 *Math.pow(2,map.scale)));
230 private function tile2lon(t:int):Number {
231 return (t/Math.pow(2,map.scale)*360-180);
233 private function tile2lat(t:int):Number {
234 var n:Number=Math.PI-2*Math.PI*t/Math.pow(2,map.scale);
235 return (180/Math.PI*Math.atan(0.5*(Math.exp(n)-Math.exp(-n))));