8f81d7d30d549515615fa3b237a564dfc04088d1
[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 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;                      // tms or bing
24                 public var blocks:Array;                        // array of regexes which are verboten
25
26                 private var count:Number=0;                     // counter incremented to provide a/b/c/d tile swapping
27                 private static const ROUNDROBIN:RegExp =/\{switch\:([^}]+)\}/;
28
29                 private var map:Map;
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.type ? params.type : 'tms';
53                         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]) { 
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]; }
128                                         }
129                                 }
130                         }
131                 }
132
133         private function missingTileError(event:Event):void {
134                         return;
135                 }
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];
141                 }
142
143                 /** Tile image has been downloaded, so start displaying it. */
144                 protected function doImgInit(event:Event):void {
145                         loadcount++;
146                         if (loadcount>MAXTILESLOADED) purgeTiles();
147                         return;
148                 }
149                 
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]);
156                                                 delete tiles[tile];
157                                                 loadcount--;
158                                         }
159                                 }
160                         }
161                 }
162
163                 
164                 // Assemble tile URL
165                 
166                 private function tileURL(tx:int,ty:int,tz:uint):String {
167                         var t:String='';
168                         var tmsy:int=Math.pow(2,tz)-1-ty;
169                         switch (scheme.toLowerCase()) {
170
171                                 case 'bing':
172                                         var u:String='';
173                                         for (var zoom:uint=tz; zoom>0; zoom--) {
174                                                 var byte:uint=0;
175                                                 var mask:uint=1<<(zoom-1);
176                                                 if ((tx & mask)!=0) byte++;
177                                                 if ((ty & mask)!=0) byte+=2;
178                                                 u+=String(byte);
179                                         }
180                                         t=baseurl.replace('{quadkey}',u); break;
181
182                                 default:
183                                         if (baseurl.indexOf('{x}')>-1) {
184                                                 t=baseurl.replace('{zoom}',map.scale).replace('{x}',tx).replace('{y}',ty).replace('{-y}',tmsy);
185                                         } else if (baseurl.indexOf('$x')>-1) {
186                                                 t=baseurl.replace('$z',map.scale).replace('$x',tx).replace('$y',ty).replace('$-y',tmsy);
187                                         } else {
188                                                 t=baseurl.replace('!',map.scale).replace('!',tx).replace('!',ty);
189                                         }
190                                         // also, someone should invent yet another variable substitution scheme
191                                         break;
192
193                         }
194                         var o:Object=new Object();
195                         if ((o=ROUNDROBIN.exec(t))) {
196                                 var prefixes:Array=o[1].split(',');
197                                 var p:String = prefixes[count % prefixes.length];
198                                 t=t.replace(ROUNDROBIN,p);
199                                 count++;
200                         }
201
202                         for each (var block:* in blocks) { if (t.match(block)) return ''; }
203                         return t;
204                 }
205                 
206                 public function get url():String {
207                         return baseurl ? baseurl : '';
208                 }
209
210                 /** Respond to nudge event by updating offset between imagery and map. */
211                 public function nudgeHandler(event:MapEvent):void {
212                         if (!baseurl) { return; }
213                         this.x+=event.params.x; this.y+=event.params.y;
214                         offset_lat=map.centre_lat-map.coord2lat(map.lat2coord(map.centre_lat)-this.y);
215                         offset_lon=map.centre_lon-map.coord2lon(map.lon2coord(map.centre_lon)-this.x);
216                         update();
217                 }
218
219                 
220                 // ------------------------------------------------------------------
221                 // Co-ordinate conversion functions
222
223                 private function lon2tile(lon:Number):int {
224                         return (Math.floor((lon+180)/360*Math.pow(2,map.scale)));
225                 }
226                 private function lat2tile(lat:Number):int { 
227                         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)));
228                 }
229                 private function tile2lon(t:int):Number {
230                         return (t/Math.pow(2,map.scale)*360-180);
231                 }
232                 private function tile2lat(t:int):Number { 
233                         var n:Number=Math.PI-2*Math.PI*t/Math.pow(2,map.scale);
234                         return (180/Math.PI*Math.atan(0.5*(Math.exp(n)-Math.exp(-n))));
235                 }
236
237         }
238 }