Support $-y for TMS imagery URLs via the UI
[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):void {
51                         baseurl=params.url;
52                         scheme =params.scheme ? params.scheme : '900913';
53                         requests=[]; waiting=loadcount=0;
54                         for (var tilename:String in tiles) {
55                                 if (tiles[tilename] is Loader) tiles[tilename].unload();
56                                 tiles[tilename]=null;
57                         }
58                         tiles={};
59                         offset_lon=offset_lat=x=y=0;
60                         while (numChildren) { removeChildAt(0); }
61                         createSprites();
62                         if (update) { this.update(); }
63                 }
64
65                 private function createSprites():void {
66                         for (var i:uint=map.MINSCALE; i<=map.MAXSCALE; i++) {
67                                 this.addChild(new Sprite());
68                         }
69                 }
70
71                 /** Toggle fading of imagery. */
72                 public function setDimming(dim:Boolean):void {
73                         alpha=dim ? 0.5 : 1;
74                 }
75                 /** Is imagery currently set faded? */
76                 public function getDimming():Boolean {
77                         return (alpha<1);
78                 }
79
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;
87                                 }
88                         }
89                         sharpening=sharpen;
90                 }
91                 
92                 /** Is sharpen filter applied? */
93                 public function getSharpen():Boolean {
94                         return sharpening;
95                 }
96
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);
101                         }
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);
104                 }
105                         
106                 /** Update bounds of tile area, and request new tiles if needed.  */
107                 
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]) { addRequest(tx,ty); }
117                                 }
118                         }
119                 }
120
121                 /** Mark that a tile needs to be loaded*/
122                 
123                 public function addRequest(tx:int,ty:int):void {
124                         tiles[map.scale+','+tx+','+ty]=true;
125                         requests.push([map.scale,tx,ty]);
126                 }
127
128                 /** Service tile queue - called on every frame to download new tiles */
129                 
130                 public function serviceQueue():void {
131                         if (waiting==MAXTILEREQUESTS || requests.length==0) { return; } //SB
132                         var r:Array, tx:int, ty:int, tz:int, l:DisplayObject;
133
134                         for (var i:uint=0; i<Math.min(requests.length, MAXTILEREQUESTS-waiting); i++) {
135                                 r=requests.shift(); tz=r[0]; tx=r[1]; ty=r[2];
136                                 if (tx>=tile_l && tx<=tile_r && ty>=tile_t && ty<=tile_b) {
137                                         // Tile is on-screen, so load
138                                         waiting++;
139                                         var loader:Loader = new Loader();
140                                         tiles[map.scale+','+tx+','+ty]=loader;
141                                         loader.contentLoaderInfo.addEventListener(Event.INIT, doImgInit, false, 0, true);
142                                         loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, missingTileError, false, 0, true);
143                                         loader.load(new URLRequest(tileURL(tx,ty,tz)), 
144                                                     new LoaderContext(true));
145                                         l=this.getChildAt(map.scale-map.MINSCALE);
146                                         Sprite(l).addChild(loader);
147                                         loader.x=map.lon2coord(tile2lon(tx));
148                                         loader.y=map.lat2coord(tile2lat(ty));
149                                         if (sharpening) { loader.filters=[sharpenFilter]; }
150                     /*
151                     var timer:Timer = new Timer(5000, 1);
152                     timer.addEventListener(TimerEvent.TIMER, function(){checkTileLoaded(map.scale,tx,ty);});
153                     timer.start();
154                                         */
155
156                                 } else {
157                                         tiles[tz+','+tx+','+ty]=false; // Map has moved between the time we wanted this tile and now, so make 
158                                                                        //it available for a future request
159                                 }
160                         }
161                 }
162
163 /* We may need something like this in the future, not sure. Trouble is if a tile doesn't get loaded on first go,
164    it will never get loaded. 
165         private function checkTileLoaded(z,x,y) {
166             if (tiles[z+','+x+','+y]==true){
167                 trace("Didn't even start getting tile: " + z+','+x+','+y);
168                 requests.push([z,x,y]);
169                 return; 
170             }   
171                 var l:Loader = tiles[z+','+x+','+y];
172                 if (l.alpha < 0.1) { 
173                         trace('Broken tile:' + z+','+x+','+y); 
174                 }
175
176         }
177         */
178         private function missingTileError(event:Event):void {
179                         waiting--;
180                         return;
181                 }
182
183                 /** Tile image has been downloaded, so start displaying it. */
184                 protected function doImgInit(event:Event):void {
185                         event.target.loader.alpha=0;
186                         var t:Timer=new Timer(10,10);
187                         t.addEventListener(TimerEvent.TIMER,function():void { upFade(DisplayObject(event.target.loader)); });
188                         t.start();
189                         waiting--;
190                         loadcount++;
191                         if (loadcount>MAXTILESLOADED) purgeTiles();
192                         return;
193                 }
194                 
195                 protected function upFade(s:DisplayObject):void {
196                         s.alpha+=0.1;
197                 }
198                 
199                 protected function purgeTiles():void {
200                         for (var tile:String in tiles) {
201                                 if (tiles[tile] is Sprite) {
202                                         var coords:Array=tile.split(','); var tz:uint=coords[0]; var tx:uint=coords[1]; var ty:uint=coords[1];
203                                         if (tz!=map.scale || tx<tile_l || tx>tile_r || ty<tile_t || ty<tile_b) {
204                                                 if (tiles[tile].parent) tiles[tile].parent.removeChild(tiles[tile]);
205                                                 delete tiles[tile];
206                                                 loadcount--;
207                                         }
208                                 }
209                         }
210                 }
211
212                 
213                 // Assemble tile URL
214                 
215                 private function tileURL(tx:int,ty:int,tz:uint):String {
216                         var t:String='';
217                         var tmsy:int=Math.pow(2,tz)-1-ty;
218                         switch (scheme.toLowerCase()) {
219
220                                 case 'microsoft':
221                                         var u:String='';
222                                         for (var zoom:uint=tz; zoom>0; zoom--) {
223                                                 var byte:uint=0;
224                                                 var mask:uint=1<<(zoom-1);
225                                                 if ((tx & mask)!=0) byte++;
226                                                 if ((ty & mask)!=0) byte+=2;
227                                                 u+=String(byte);
228                                         }
229                                         t=baseurl.replace('$quadkey',u); break;
230
231                                 case 'tms':
232                                         t=baseurl.replace('$z',map.scale).replace('$x',tx).replace('$y',tmsy);
233                                         break;
234
235                                 default:
236                                         if (baseurl.indexOf('$x')>-1) {
237                                                 t=baseurl.replace('$z',map.scale).replace('$x',tx).replace('$y',ty).replace('$-y',tmsy);
238                                         } else {
239                                                 t=baseurl.replace('!',map.scale).replace('!',tx).replace('!',ty);
240                                         }
241                                         break;
242
243                         }
244                         for each (var block:* in blocks) { if (t.match(block)) return ''; }
245                         return t;
246                 }
247                 
248                 public function get url():String {
249                         return baseurl ? baseurl : '';
250                 }
251
252                 /** Respond to nudge event by updating offset between imagery and map. */
253                 public function nudgeHandler(event:MapEvent):void {
254                         if (!baseurl) { return; }
255                         this.x+=event.params.x; this.y+=event.params.y;
256                         offset_lat=map.centre_lat-map.coord2lat(map.lat2coord(map.centre_lat)-this.y);
257                         offset_lon=map.centre_lon-map.coord2lon(map.lon2coord(map.centre_lon)-this.x);
258                         update();
259                 }
260
261                 
262                 // ------------------------------------------------------------------
263                 // Co-ordinate conversion functions
264
265                 private function lon2tile(lon:Number):int {
266                         return (Math.floor((lon+180)/360*Math.pow(2,map.scale)));
267                 }
268                 private function lat2tile(lat:Number):int { 
269                         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)));
270                 }
271                 private function tile2lon(t:int):Number {
272                         return (t/Math.pow(2,map.scale)*360-180);
273                 }
274                 private function tile2lat(t:int):Number { 
275                         var n:Number=Math.PI-2*Math.PI*t/Math.pow(2,map.scale);
276                         return (180/Math.PI*Math.atan(0.5*(Math.exp(n)-Math.exp(-n))));
277                 }
278
279         }
280 }