Merge branch 'master' of github.com:systemed/potlatch2
[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         import flash.text.TextField;
10         import flash.text.TextFormat;
11
12         import net.systemeD.potlatch2.collections.*;
13         /* -------
14            This currently requires potlatch2.collections.Imagery and
15                                    potlatch2.collections.CollectionEvent which break Halcyon.
16            ------- */
17
18     public class TileSet extends Sprite {
19
20                 public var tile_l:int;
21                 public var tile_r:int;
22                 public var tile_b:int;
23                 public var tile_t:int;
24
25                 private var offset_lon:Number=0;
26                 private var offset_lat:Number=0;
27
28                 private var tiles:Object={};            // key is "z,x,y"; value "true" if queued, or reference to loader object if requested
29                 private var loadcount:int=0;            // number of tiles fully downloaded
30                 private var baseurl:String;                     // e.g. http://npe.openstreetmap.org/$z/$x/$y.png
31                 private var scheme:String;                      // tms or bing
32                 public var blocks:Array;                        // array of regexes which are verboten
33
34                 private var count:Number=0;                     // counter incremented to provide a/b/c/d tile swapping
35                 private static const ROUNDROBIN:RegExp =/\{switch\:([^}]+)\}/;
36
37                 private var _map:Map;
38                 private var _overlay:Sprite;
39                 private const MAXTILESLOADED:uint=30;
40
41                 private var sharpenFilter:BitmapFilter = new ConvolutionFilter(3, 3, 
42                         [0, -1, 0,
43             -1, 5, -1,
44              0, -1, 0], 0);
45                 private var sharpening:Boolean = false;
46                 // http://flylib.com/books/en/2.701.1.170/1/
47
48         public function TileSet(map:Map, overlay:Sprite) {
49                         _map=map;
50                         _overlay=overlay;
51                         createSprites();
52                         _map.addEventListener(MapEvent.NUDGE_BACKGROUND, nudgeHandler);
53                         _map.addEventListener(MapEvent.MOVE_END, moveHandler);
54                         _map.addEventListener(MapEvent.RESIZE, resizeHandler);
55                 }
56         
57                 /** @param params Currently includes "url" and "scheme"
58                  * @param update Trigger update now?
59                  * @param dim Start with imagery faded?
60                  * @param sharpen Start with sharpen filter applied?
61                  */
62                 public function init(params:Object, update:Boolean=false):void {
63                         baseurl=params.url;
64                         scheme =params.type ? params.type : 'tms';
65                         loadcount=0;
66                         for (var tilename:String in tiles) {
67                                 if (tiles[tilename] is Loader) tiles[tilename].unload();
68                                 tiles[tilename]=null;
69                         }
70                         tiles={};
71                         offset_lon=offset_lat=x=y=0;
72                         while (numChildren) { removeChildAt(0); }
73                         createSprites();
74                         if (update) { this.update(); }
75                 }
76
77                 private function createSprites():void {
78                         for (var i:uint=_map.MINSCALE_TILES; i<=_map.MAXSCALE; i++) {
79                                 this.addChild(new Sprite());
80                         }
81                 }
82
83                 /** Toggle fading of imagery. */
84                 public function setDimming(dim:Boolean):void {
85                         alpha=dim ? 0.5 : 1;
86                 }
87                 /** Is imagery currently set faded? */
88                 public function getDimming():Boolean {
89                         return (alpha<1);
90                 }
91
92         /** Toggle sharpen filter. */
93                 public function setSharpen(sharpen:Boolean):void {
94                         var f:Array=[]; if (sharpen) { f=[sharpenFilter]; }
95                         for (var i:uint=0; i<numChildren; i++) {
96                                 var s:Sprite=Sprite(getChildAt(i));
97                                 for (var j:uint=0; j<s.numChildren; j++) {
98                                         s.getChildAt(j).filters=f;
99                                 }
100                         }
101                         sharpening=sharpen;
102                 }
103                 
104                 /** Is sharpen filter applied? */
105                 public function getSharpen():Boolean {
106                         return sharpening;
107                 }
108
109                 /** Set zoom scale (no update triggerd). */
110                 public function changeScale(scale:uint):void {
111                         for (var i:uint=_map.MINSCALE_TILES; i<=_map.MAXSCALE; i++) {
112                                 this.getChildAt(i-_map.MINSCALE_TILES).visible=(scale==i);
113                         }
114                         x=_map.lon2coord(_map.centre_lon+offset_lon)-_map.lon2coord(_map.centre_lon);
115                         y=_map.lat2coord(_map.centre_lat+offset_lat)-_map.lat2coord(_map.centre_lat);
116                 }
117                         
118                 /** Update bounds of tile area, and request new tiles if needed.  */
119                 
120                 public function update():void {
121                         if (!baseurl) { return; }
122                         tile_l=lon2tile(_map.edge_l-offset_lon);
123                         tile_r=lon2tile(_map.edge_r-offset_lon);
124                         tile_t=lat2tile(_map.edge_t-offset_lat);
125                         tile_b=lat2tile(_map.edge_b-offset_lat);
126                         for (var tx:int=tile_l; tx<=tile_r; tx++) {
127                                 for (var ty:int=tile_t; ty<=tile_b; ty++) {
128                                         if (!tiles[_map.scale+','+tx+','+ty]) { 
129                                                 var loader:Loader = new Loader();
130                                                 tiles[_map.scale+','+tx+','+ty]=loader;
131                                                 loader.contentLoaderInfo.addEventListener(Event.INIT, doImgInit, false, 0, true);
132                                                 loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, missingTileError, false, 0, true);
133                                                 loader.contentLoaderInfo.addEventListener(HTTPStatusEvent.HTTP_STATUS, function(e:HTTPStatusEvent):void { tileLoadStatus(e,_map.scale,tx,ty); }, false, 0, true);
134                                                 loader.load(new URLRequest(tileURL(tx,ty,_map.scale)), 
135                                                             new LoaderContext(true));
136                                                 Sprite(this.getChildAt(_map.scale-_map.MINSCALE_TILES)).addChild(loader);
137                                                 loader.x=_map.lon2coord(tile2lon(tx));
138                                                 loader.y=_map.lat2coord(tile2lat(ty));
139                                                 if (sharpening) { loader.filters=[sharpenFilter]; }
140                                         }
141                                 }
142                         }
143                 }
144
145         private function missingTileError(event:Event):void {
146                         return;
147                 }
148                 private function tileLoadStatus(event:HTTPStatusEvent,z:int,x:int,y:int):void {
149                         if (event.status==200) return;                          // fine, carry on
150                         if (event.status==404) return;                          // doesn't exist, so ignore forever
151                         // Dodgy tile response - probably a 502/503 from Bing - so can be retried
152                         delete tiles[z+','+x+','+y];
153                 }
154
155                 /** Tile image has been downloaded, so start displaying it. */
156                 protected function doImgInit(event:Event):void {
157                         loadcount++;
158                         if (loadcount>MAXTILESLOADED) purgeTiles();
159                         return;
160                 }
161                 
162                 protected function purgeTiles():void {
163                         for (var tile:String in tiles) {
164                                 if (tiles[tile] is Sprite) {
165                                         var coords:Array=tile.split(','); var tz:uint=coords[0]; var tx:uint=coords[1]; var ty:uint=coords[1];
166                                         if (tz!=_map.scale || tx<tile_l || tx>tile_r || ty<tile_t || ty<tile_b) {
167                                                 if (tiles[tile].parent) tiles[tile].parent.removeChild(tiles[tile]);
168                                                 delete tiles[tile];
169                                                 loadcount--;
170                                         }
171                                 }
172                         }
173                 }
174
175                 
176                 // Assemble tile URL
177                 
178                 private function tileURL(tx:int,ty:int,tz:uint):String {
179                         var t:String='';
180                         var tmsy:int=Math.pow(2,tz)-1-ty;
181                         switch (scheme.toLowerCase()) {
182
183                                 case 'bing':
184                                         var u:String='';
185                                         for (var zoom:uint=tz; zoom>0; zoom--) {
186                                                 var byte:uint=0;
187                                                 var mask:uint=1<<(zoom-1);
188                                                 if ((tx & mask)!=0) byte++;
189                                                 if ((ty & mask)!=0) byte+=2;
190                                                 u+=String(byte);
191                                         }
192                                         t=baseurl.replace('{quadkey}',u); break;
193
194                                 default:
195                                         if (baseurl.indexOf('{x}')>-1) {
196                                                 t=baseurl.replace('{zoom}',_map.scale).replace('{x}',tx).replace('{y}',ty).replace('{-y}',tmsy);
197                                         } else if (baseurl.indexOf('$x')>-1) {
198                                                 t=baseurl.replace('$z',_map.scale).replace('$x',tx).replace('$y',ty).replace('$-y',tmsy);
199                                         } else {
200                                                 t=baseurl.replace('!',_map.scale).replace('!',tx).replace('!',ty);
201                                         }
202                                         // also, someone should invent yet another variable substitution scheme
203                                         break;
204
205                         }
206                         var o:Object=new Object();
207                         if ((o=ROUNDROBIN.exec(t))) {
208                                 var prefixes:Array=o[1].split(',');
209                                 var p:String = prefixes[count % prefixes.length];
210                                 t=t.replace(ROUNDROBIN,p);
211                                 count++;
212                         }
213
214                         for each (var block:* in blocks) { if (t.match(block)) return ''; }
215                         return t;
216                 }
217                 
218                 public function get url():String {
219                         return baseurl ? baseurl : '';
220                 }
221
222                 /** Respond to nudge event by updating offset between imagery and map. */
223                 public function nudgeHandler(event:MapEvent):void {
224                         if (!baseurl) { return; }
225                         this.x+=event.params.x; this.y+=event.params.y;
226                         offset_lat=_map.centre_lat-_map.coord2lat(_map.lat2coord(_map.centre_lat)-this.y);
227                         offset_lon=_map.centre_lon-_map.coord2lon(_map.lon2coord(_map.centre_lon)-this.x);
228                         update();
229                 }
230
231                 
232                 // ------------------------------------------------------------------
233                 // Co-ordinate conversion functions
234
235                 private function lon2tile(lon:Number):int {
236                         return (Math.floor((lon+180)/360*Math.pow(2,_map.scale)));
237                 }
238                 private function lat2tile(lat:Number):int { 
239                         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)));
240                 }
241                 private function tile2lon(t:int):Number {
242                         return (t/Math.pow(2,_map.scale)*360-180);
243                 }
244                 private function tile2lat(t:int):Number { 
245                         var n:Number=Math.PI-2*Math.PI*t/Math.pow(2,_map.scale);
246                         return (180/Math.PI*Math.atan(0.5*(Math.exp(n)-Math.exp(-n))));
247                 }
248
249                 // ------------------------------------------------------------------
250                 // Attribution/terms management
251                 // (moved from Imagery.as)
252
253                 private var _selected:Object={};
254                 public function get selected():Object { return _selected; }
255
256                 public function setAttribution():void {
257                         var tf:TextField=TextField(_overlay.getChildAt(0));
258                         tf.text='';
259                         tf.visible=false;
260                         if (!_selected.attribution) return;
261                         var attr:Array=[];
262                         if (_selected.attribution.providers) {
263                                 // Bing attribution scheme
264                                 for (var provider:String in _selected.attribution.providers) {
265                                         for each (var bounds:Array in _selected.attribution.providers[provider]) {
266                                                 if (_map.scale>=bounds[0] && _map.scale<=bounds[1] &&
267                                                   ((_map.edge_l>bounds[3] && _map.edge_l<bounds[5]) ||
268                                                    (_map.edge_r>bounds[3] && _map.edge_r<bounds[5]) ||
269                                            (_map.edge_l<bounds[3] && _map.edge_r>bounds[5])) &&
270                                                   ((_map.edge_b>bounds[2] && _map.edge_b<bounds[4]) ||
271                                                    (_map.edge_t>bounds[2] && _map.edge_t<bounds[4]) ||
272                                                    (_map.edge_b<bounds[2] && _map.edge_t>bounds[4]))) {
273                                                         attr.push(provider);
274                                                 }
275                                         }
276                                 }
277                         }
278                         if (attr.length==0) return;
279                         tf.visible=true;
280                         tf.text="Background "+attr.join(", ");
281                         positionAttribution();
282                         dispatchEvent(new MapEvent(MapEvent.BUMP, { y: tf.textHeight }));       // don't let the toolbox obscure it
283                 }
284                 public function positionAttribution():void {
285                         if (!_selected.attribution) return;
286                         var tf:TextField=TextField(_overlay.getChildAt(0));
287                         tf.x=_map.mapwidth  - 5 - tf.textWidth;
288                         tf.y=_map.mapheight - 5 - tf.textHeight;
289                 }
290
291                 public function setLogo():void {
292                         while (_overlay.numChildren>2) { _overlay.removeChildAt(2); }
293                         if (!_selected.logoData) return;
294                         var logo:Sprite=new Sprite();
295                         logo.addChild(new Bitmap(_selected.logoData));
296                         if (_selected.attribution.url) { logo.buttonMode=true; logo.addEventListener(MouseEvent.CLICK, launchLogoLink, false, 0, true); }
297                         _overlay.addChild(logo);
298                         positionLogo();
299                 }
300                 public function positionLogo():void {
301                         if (_overlay.numChildren<3) return;
302                         _overlay.getChildAt(2).x=5;
303                         _overlay.getChildAt(2).y=_map.mapheight - 5 - _selected.logoHeight - (_selected.terms_url ? 10 : 0);
304                 }
305                 private function launchLogoLink(e:Event):void {
306                         if (!_selected.attribution.url) return;
307                         navigateToURL(new URLRequest(_selected.attribution.url), '_blank');
308                 }
309                 public function setTerms():void {
310                         var terms:TextField=TextField(_overlay.getChildAt(1));
311                         terms.visible=false;
312                         if (!_selected.attribution) { terms.text=''; return; }
313                         terms.visible=true;
314                         if (_selected.attribution && _selected.attribution.text) { terms.text=_selected.attribution.text; }
315                         else { terms.text="Background terms of use"; }
316                         positionTerms();
317                         terms.addEventListener(MouseEvent.CLICK, launchTermsLink, false, 0, true);
318                 }
319                 private function positionTerms():void {
320                         _overlay.getChildAt(1).x=5;
321                         _overlay.getChildAt(1).y=_map.mapheight - 15;
322                 }
323                 private function launchTermsLink(e:Event):void {
324                         if (!_selected.attribution.url) return;
325                         navigateToURL(new URLRequest(_selected.attribution.url), '_blank');
326                 }
327
328                 public function resizeHandler(event:MapEvent):void {
329                         positionLogo();
330                         positionTerms();
331                         positionAttribution();
332                 }
333                 private function moveHandler(event:MapEvent):void {
334                         setAttribution();
335                         // strictly speaking we should review the collection on every move, but slow
336                         // dispatchEvent(new Event("collection_changed"));
337                 }
338
339                 // Create overlay sprite
340                 public static function overlaySprite():Sprite {
341                         var overlay:Sprite=new Sprite();
342                         var attribution:TextField=new TextField();
343                         attribution.width=220; attribution.height=300;
344                         attribution.multiline=true;
345                         attribution.wordWrap=true;
346                         attribution.selectable=false;
347                         attribution.defaultTextFormat=new TextFormat("_sans", 9, 0, false, false, false);
348                         attribution.visible=false;
349                         overlay.addChild(attribution);
350                         var terms:TextField=new TextField();
351                         terms.width=200; terms.height=15;
352                         terms.selectable=false;
353                         terms.defaultTextFormat=new TextFormat("_sans", 9, 0, false, false, true);
354                         terms.visible=false;
355                         overlay.addChild(terms);
356                         return overlay;
357                 }
358
359                 // ------------------------------------------------------------------
360                 // Choose a new background
361                 // (moved from setBackground in Imagery.as)
362                 
363                 public function setBackgroundFromImagery(bg:Object,remember:Boolean):void {
364                         // set background
365                         _selected=bg;
366 //                      dispatchEvent(new CollectionEvent(CollectionEvent.SELECT, bg));
367                         _map.tileset.init(bg, bg!='');
368                         // update attribution and logo
369                         _overlay.visible=bg.hasOwnProperty('attribution');
370                         setLogo(); setAttribution(); setTerms();
371                         // save as SharedObject for next time
372                         if (remember) {
373                                 var obj:SharedObject = SharedObject.getLocal("user_state","/");
374                                 obj.setProperty('background_url' ,String(bg.url));
375                                 obj.setProperty('background_name',String(bg.name));
376                                 try { obj.flush(); } catch (e:Error) {}
377                         }
378                 }
379
380         }
381 }