Add support for TMS tile-numbering scheme
[potlatch2.git] / net / systemeD / halcyon / TileSet.as
1 package net.systemeD.halcyon {
2
3         import flash.display.*;
4         import flash.events.*;
5         import flash.filters.*;
6         import flash.net.*;
7         import flash.system.LoaderContext;
8         import flash.utils.Timer;
9
10     public class TileSet extends Sprite {
11
12                 public var tile_l:int;
13                 public var tile_r:int;
14                 public var tile_b:int;
15                 public var tile_t:int;
16
17                 private var offset_lon:Number=0;
18                 private var offset_lat:Number=0;
19
20                 private var requests:Array=[];
21                 private var tiles:Object={};            // key is "z,x,y"; value "true" if queued, or reference to loader object if requested
22                 private var waiting:int=0;                      // number of tiles currently being downloaded
23                 private var loadcount:int=0;            // number of tiles fully downloaded
24                 private var baseurl:String;                     // e.g. http://npe.openstreetmap.org/$z/$x/$y.png
25                 private var scheme:String;                      // 900913 or microsoft
26                 public var blocks:Array;                        // array of regexes which are verboten
27
28                 private var map:Map;
29                 private const MAXTILEREQUESTS:uint= 4;
30                 private const MAXTILESLOADED:uint=30;
31
32                 private var sharpenFilter:BitmapFilter = new ConvolutionFilter(3, 3, 
33                         [0, -1, 0,
34             -1, 5, -1,
35              0, -1, 0], 0);
36                 private var sharpening:Boolean = false;
37                 // http://flylib.com/books/en/2.701.1.170/1/
38
39         public function TileSet(map:Map) {
40                         this.map=map;
41                         createSprites();
42                         map.addEventListener(MapEvent.NUDGE_BACKGROUND, nudgeHandler);
43                 }
44         
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?
49                  */
50                 public function init(params:Object, update:Boolean=false, dim:Boolean=true, sharpen:Boolean=false):void {
51                         setDimming(dim);
52                         sharpening=sharpen;
53                         baseurl=params.url;
54                         scheme =params.scheme ? params.scheme : '900913';
55                         tiles={};
56                         offset_lon=offset_lat=x=y=0;
57                         while (numChildren) { removeChildAt(0); }
58                         createSprites();
59                         if (update) { this.update(); }
60                 }
61
62                 private function createSprites():void {
63                         for (var i:uint=map.MINSCALE; i<=map.MAXSCALE; i++) {
64                                 this.addChild(new Sprite());
65                         }
66                 }
67
68                 /** Toggle fading of imagery. */
69                 public function setDimming(dim:Boolean):void {
70                         alpha=dim ? 0.5 : 1;
71                 }
72                 /** Is imagery currently set faded? */
73                 public function getDimming():Boolean {
74                         return (alpha<1);
75                 }
76
77         /** Toggle sharpen filter. */
78                 public function setSharpen(sharpen:Boolean):void {
79                         var f:Array=[]; if (sharpen) { f=[sharpenFilter]; }
80                         for (var i:uint=0; i<numChildren; i++) {
81                                 var s:Sprite=Sprite(getChildAt(i));
82                                 for (var j:uint=0; j<s.numChildren; j++) {
83                                         s.getChildAt(j).filters=f;
84                                 }
85                         }
86                         sharpening=sharpen;
87                 }
88                 
89                 /** Is sharpen filter applied? */
90                 public function getSharpen():Boolean {
91                         return sharpening;
92                 }
93
94                 /** Set zoom scale (no update triggerd). */
95                 public function changeScale(scale:uint):void {
96                         for (var i:uint=map.MINSCALE; i<=map.MAXSCALE; i++) {
97                                 this.getChildAt(i-map.MINSCALE).visible=(scale==i);
98                         }
99                         x=map.lon2coord(map.centre_lon+offset_lon)-map.lon2coord(map.centre_lon);
100                         y=map.lat2coord(map.centre_lat+offset_lat)-map.lat2coord(map.centre_lat);
101                 }
102                         
103                 /** Update bounds of tile area, and request new tiles if needed.  */
104                 
105                 public function update():void {
106                         if (!baseurl) { return; }
107                         tile_l=lon2tile(map.edge_l-offset_lon);
108                         tile_r=lon2tile(map.edge_r-offset_lon);
109                         tile_t=lat2tile(map.edge_t-offset_lat);
110                         tile_b=lat2tile(map.edge_b-offset_lat);
111                         for (var tx:int=tile_l; tx<=tile_r; tx++) {
112                                 for (var ty:int=tile_t; ty<=tile_b; ty++) {
113                                         if (!tiles[map.scale+','+tx+','+ty]) { addRequest(tx,ty); }
114                                 }
115                         }
116                 }
117
118                 /** Mark that a tile needs to be loaded*/
119                 
120                 public function addRequest(tx:int,ty:int):void {
121                         tiles[map.scale+','+tx+','+ty]=true;
122                         requests.push([map.scale,tx,ty]);
123                 }
124
125                 /** Service tile queue - called on every frame to download new tiles */
126                 
127                 public function serviceQueue():void {
128                         if (waiting==MAXTILEREQUESTS || requests.length==0) { return; } //SB
129                         var r:Array, tx:int, ty:int, tz:int, l:DisplayObject;
130
131                         for (var i:uint=0; i<Math.min(requests.length, MAXTILEREQUESTS-waiting); i++) {
132                                 r=requests.shift(); tz=r[0]; tx=r[1]; ty=r[2];
133                                 if (tx>=tile_l && tx<=tile_r && ty>=tile_t && ty<=tile_b) {
134                                         // Tile is on-screen, so load
135                                         waiting++;
136                                         var loader:Loader = new Loader();
137                                         tiles[map.scale+','+tx+','+ty]=loader;
138                                         loader.contentLoaderInfo.addEventListener(Event.INIT, doImgInit);
139                         loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, missingTileError);
140                                         loader.load(new URLRequest(tileURL(tx,ty,tz)), 
141                                                     new LoaderContext(true));
142                                         l=this.getChildAt(map.scale-map.MINSCALE);
143                                         Sprite(l).addChild(loader);
144                                         loader.x=map.lon2coord(tile2lon(tx));
145                                         loader.y=map.lat2coord(tile2lat(ty));
146                                         if (sharpening) { loader.filters=[sharpenFilter]; }
147                     /*
148                     var timer:Timer = new Timer(5000, 1);
149                     timer.addEventListener(TimerEvent.TIMER, function(){checkTileLoaded(map.scale,tx,ty);});
150                     timer.start();
151                                         */
152
153                                 } else {
154                                         tiles[tz+','+tx+','+ty]=false; // Map has moved between the time we wanted this tile and now, so make 
155                                                                        //it available for a future request
156                                 }
157                         }
158                 }
159
160 /* We may need something like this in the future, not sure. Trouble is if a tile doesn't get loaded on first go,
161    it will never get loaded. 
162         private function checkTileLoaded(z,x,y) {
163             if (tiles[z+','+x+','+y]==true){
164                 trace("Didn't even start getting tile: " + z+','+x+','+y);
165                 requests.push([z,x,y]);
166                 return; 
167             }   
168                 var l:Loader = tiles[z+','+x+','+y];
169                 if (l.alpha < 0.1) { 
170                         trace('Broken tile:' + z+','+x+','+y); 
171                 }
172
173         }
174         */
175         private function missingTileError(event:Event):void {
176                         waiting--;
177                         return;
178                 }
179
180                 /** Tile image has been downloaded, so start displaying it. */
181                 protected function doImgInit(event:Event):void {
182                         event.target.loader.alpha=0;
183                         var t:Timer=new Timer(10,10);
184                         t.addEventListener(TimerEvent.TIMER,function():void { upFade(DisplayObject(event.target.loader)); });
185                         t.start();
186                         waiting--;
187                         loadcount++;
188                         if (loadcount>MAXTILESLOADED) purgeTiles();
189                         return;
190                 }
191                 
192                 protected function upFade(s:DisplayObject):void {
193                         s.alpha+=0.1;
194                 }
195                 
196                 protected function purgeTiles():void {
197                         for (var tile:String in tiles) {
198                                 if (tiles[tile] is Sprite) {
199                                         var coords:Array=tile.split(','); var tz:uint=coords[0]; var tx:uint=coords[1]; var ty:uint=coords[1];
200                                         if (tz!=map.scale || tx<tile_l || tx>tile_r || ty<tile_t || ty<tile_b) {
201                                                 if (tiles[tile].parent) tiles[tile].parent.removeChild(tiles[tile]);
202                                                 delete tiles[tile];
203                                                 loadcount--;
204                                         }
205                                 }
206                         }
207                 }
208
209                 
210                 // Assemble tile URL
211                 
212                 private function tileURL(tx:int,ty:int,tz:uint):String {
213                         var t:String='';
214                         switch (scheme.toLowerCase()) {
215
216                                 case 'microsoft':
217                                         var u:String='';
218                                         for (var zoom:uint=tz; zoom>0; zoom--) {
219                                                 var byte:uint=0;
220                                                 var mask:uint=1<<(zoom-1);
221                                                 if ((tx & mask)!=0) byte++;
222                                                 if ((ty & mask)!=0) byte+=2;
223                                                 u+=String(byte);
224                                         }
225                                         t=baseurl.replace('$quadkey',u); break;
226
227                                 case 'tms':
228                                         ty=Math.pow(2,tz)-1-ty;
229                                         t=baseurl.replace('$z',map.scale).replace('$x',tx).replace('$y',ty);
230                                         break;
231
232                                 default:
233                                         if (baseurl.indexOf('$x')>-1) {
234                                                 t=baseurl.replace('$z',map.scale).replace('$x',tx).replace('$y',ty);
235                                         } else {
236                                                 t=baseurl.replace('!',map.scale).replace('!',tx).replace('!',ty);
237                                         }
238                                         break;
239
240                         }
241                         for each (var block:* in blocks) { if (t.match(block)) return ''; }
242                         return t;
243                 }
244                 
245                 public function get url():String {
246                         return baseurl ? baseurl : '';
247                 }
248
249                 /** Respond to nudge event by updating offset between imagery and map. */
250                 public function nudgeHandler(event:MapEvent):void {
251                         if (!baseurl) { return; }
252                         this.x+=event.params.x; this.y+=event.params.y;
253                         offset_lat=map.centre_lat-map.coord2lat(map.lat2coord(map.centre_lat)-this.y);
254                         offset_lon=map.centre_lon-map.coord2lon(map.lon2coord(map.centre_lon)-this.x);
255                         update();
256                 }
257
258                 
259                 // ------------------------------------------------------------------
260                 // Co-ordinate conversion functions
261
262                 private function lon2tile(lon:Number):int {
263                         return (Math.floor((lon+180)/360*Math.pow(2,map.scale)));
264                 }
265                 private function lat2tile(lat:Number):int { 
266                         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)));
267                 }
268                 private function tile2lon(t:int):Number {
269                         return (t/Math.pow(2,map.scale)*360-180);
270                 }
271                 private function tile2lat(t:int):Number { 
272                         var n:Number=Math.PI-2*Math.PI*t/Math.pow(2,map.scale);
273                         return (180/Math.PI*Math.atan(0.5*(Math.exp(n)-Math.exp(-n))));
274                 }
275
276         }
277 }