1 package net.systemeD.halcyon {
3 import flash.display.*;
5 import flash.filters.*;
7 import flash.system.LoaderContext;
8 import flash.utils.Timer;
9 import flash.text.TextField;
10 import flash.text.TextFormat;
12 import net.systemeD.potlatch2.collections.*;
14 This currently requires potlatch2.collections.Imagery and
15 potlatch2.collections.CollectionEvent which break Halcyon.
18 public class TileSet extends Sprite {
20 public var tile_l:int;
21 public var tile_r:int;
22 public var tile_b:int;
23 public var tile_t:int;
25 private var offset_lon:Number=0;
26 private var offset_lat:Number=0;
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
34 private var count:Number=0; // counter incremented to provide a/b/c/d tile swapping
35 private static const ROUNDROBIN:RegExp =/\{switch\:([^}]+)\}/;
38 private var _overlay:Sprite;
39 private const MAXTILESLOADED:uint=30;
41 private var sharpenFilter:BitmapFilter = new ConvolutionFilter(3, 3,
45 private var sharpening:Boolean = false;
46 // http://flylib.com/books/en/2.701.1.170/1/
48 public function TileSet(map:Map, overlay:Sprite) {
52 _map.addEventListener(MapEvent.NUDGE_BACKGROUND, nudgeHandler);
53 _map.addEventListener(MapEvent.MOVE_END, moveHandler);
54 _map.addEventListener(MapEvent.RESIZE, resizeHandler);
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?
62 public function init(params:Object, update:Boolean=false):void {
64 scheme =params.type ? params.type : 'tms';
66 for (var tilename:String in tiles) {
67 if (tiles[tilename] is Loader) tiles[tilename].unload();
71 offset_lon=offset_lat=x=y=0;
72 while (numChildren) { removeChildAt(0); }
74 if (update) { this.update(); }
77 private function createSprites():void {
78 for (var i:uint=_map.MINSCALE; i<=_map.MAXSCALE; i++) {
79 this.addChild(new Sprite());
83 /** Toggle fading of imagery. */
84 public function setDimming(dim:Boolean):void {
87 /** Is imagery currently set faded? */
88 public function getDimming():Boolean {
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;
104 /** Is sharpen filter applied? */
105 public function getSharpen():Boolean {
109 /** Set zoom scale (no update triggerd). */
110 public function changeScale(scale:uint):void {
111 for (var i:uint=_map.MINSCALE; i<=_map.MAXSCALE; i++) {
112 this.getChildAt(i-_map.MINSCALE).visible=(scale==i);
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);
118 /** Update bounds of tile area, and request new tiles if needed. */
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)).addChild(loader);
137 loader.x=_map.lon2coord(tile2lon(tx));
138 loader.y=_map.lat2coord(tile2lat(ty));
139 if (sharpening) { loader.filters=[sharpenFilter]; }
145 private function missingTileError(event:Event):void {
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];
155 /** Tile image has been downloaded, so start displaying it. */
156 protected function doImgInit(event:Event):void {
158 if (loadcount>MAXTILESLOADED) purgeTiles();
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]);
178 private function tileURL(tx:int,ty:int,tz:uint):String {
180 var tmsy:int=Math.pow(2,tz)-1-ty;
181 switch (scheme.toLowerCase()) {
185 for (var zoom:uint=tz; zoom>0; zoom--) {
187 var mask:uint=1<<(zoom-1);
188 if ((tx & mask)!=0) byte++;
189 if ((ty & mask)!=0) byte+=2;
192 t=baseurl.replace('{quadkey}',u); break;
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);
200 t=baseurl.replace('!',_map.scale).replace('!',tx).replace('!',ty);
202 // also, someone should invent yet another variable substitution scheme
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);
214 for each (var block:* in blocks) { if (t.match(block)) return ''; }
218 public function get url():String {
219 return baseurl ? baseurl : '';
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);
232 // ------------------------------------------------------------------
233 // Co-ordinate conversion functions
235 private function lon2tile(lon:Number):int {
236 return (Math.floor((lon+180)/360*Math.pow(2,_map.scale)));
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)));
241 private function tile2lon(t:int):Number {
242 return (t/Math.pow(2,_map.scale)*360-180);
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))));
249 // ------------------------------------------------------------------
250 // Attribution/terms management
251 // (moved from Imagery.as)
253 private var _selected:Object={};
254 public function get selected():Object { return _selected; }
256 public function setAttribution():void {
257 var tf:TextField=TextField(_overlay.getChildAt(0));
259 if (!_selected.attribution) return;
261 if (_selected.attribution.providers) {
262 // Bing attribution scheme
263 for (var provider:String in _selected.attribution.providers) {
264 for each (var bounds:Array in _selected.attribution.providers[provider]) {
265 if (_map.scale>=bounds[0] && _map.scale<=bounds[1] &&
266 ((_map.edge_l>bounds[3] && _map.edge_l<bounds[5]) ||
267 (_map.edge_r>bounds[3] && _map.edge_r<bounds[5]) ||
268 (_map.edge_l<bounds[3] && _map.edge_r>bounds[5])) &&
269 ((_map.edge_b>bounds[2] && _map.edge_b<bounds[4]) ||
270 (_map.edge_t>bounds[2] && _map.edge_t<bounds[4]) ||
271 (_map.edge_b<bounds[2] && _map.edge_t>bounds[4]))) {
277 if (attr.length==0) return;
278 tf.text="Background "+attr.join(", ");
279 positionAttribution();
280 dispatchEvent(new MapEvent(MapEvent.BUMP, { y: tf.textHeight })); // don't let the toolbox obscure it
282 public function positionAttribution():void {
283 if (!_selected.attribution) return;
284 var tf:TextField=TextField(_overlay.getChildAt(0));
285 tf.x=_map.mapwidth - 5 - tf.textWidth;
286 tf.y=_map.mapheight - 5 - tf.textHeight;
289 public function setLogo():void {
290 while (_overlay.numChildren>2) { _overlay.removeChildAt(2); }
291 if (!_selected.logoData) return;
292 var logo:Sprite=new Sprite();
293 logo.addChild(new Bitmap(_selected.logoData));
294 if (_selected.attribution.url) { logo.buttonMode=true; logo.addEventListener(MouseEvent.CLICK, launchLogoLink, false, 0, true); }
295 _overlay.addChild(logo);
298 public function positionLogo():void {
299 if (_overlay.numChildren<3) return;
300 _overlay.getChildAt(2).x=5;
301 _overlay.getChildAt(2).y=_map.mapheight - 5 - _selected.logoHeight - (_selected.terms_url ? 10 : 0);
303 private function launchLogoLink(e:Event):void {
304 if (!_selected.attribution.url) return;
305 navigateToURL(new URLRequest(_selected.attribution.url), '_blank');
307 public function setTerms():void {
308 var terms:TextField=TextField(_overlay.getChildAt(1));
309 if (!_selected.attribution) { terms.text=''; return; }
310 if (_selected.attribution && _selected.attribution.text) { terms.text=_selected.attribution.text; }
311 else { terms.text="Background terms of use"; }
313 terms.addEventListener(MouseEvent.CLICK, launchTermsLink, false, 0, true);
315 private function positionTerms():void {
316 _overlay.getChildAt(1).x=5;
317 _overlay.getChildAt(1).y=_map.mapheight - 15;
319 private function launchTermsLink(e:Event):void {
320 if (!_selected.attribution.url) return;
321 navigateToURL(new URLRequest(_selected.attribution.url), '_blank');
324 public function resizeHandler(event:MapEvent):void {
327 positionAttribution();
329 private function moveHandler(event:MapEvent):void {
331 // strictly speaking we should review the collection on every move, but slow
332 // dispatchEvent(new Event("collection_changed"));
335 // Create overlay sprite
336 public static function overlaySprite():Sprite {
337 var overlay:Sprite=new Sprite();
338 var attribution:TextField=new TextField();
339 attribution.width=220; attribution.height=300;
340 attribution.multiline=true;
341 attribution.wordWrap=true;
342 attribution.selectable=false;
343 attribution.defaultTextFormat=new TextFormat("_sans", 9, 0, false, false, false);
344 overlay.addChild(attribution);
345 var terms:TextField=new TextField();
346 terms.width=200; terms.height=15;
347 terms.selectable=false;
348 terms.defaultTextFormat=new TextFormat("_sans", 9, 0, false, false, true);
349 overlay.addChild(terms);
353 // ------------------------------------------------------------------
354 // Choose a new background
355 // (moved from setBackground in Imagery.as)
357 public function setBackgroundFromImagery(bg:Object,remember:Boolean):void {
360 // dispatchEvent(new CollectionEvent(CollectionEvent.SELECT, bg));
361 _map.tileset.init(bg, bg!='');
362 // update attribution and logo
363 _overlay.visible=bg.hasOwnProperty('attribution');
364 setLogo(); setAttribution(); setTerms();
365 // save as SharedObject for next time
367 var obj:SharedObject = SharedObject.getLocal("user_state","/");
368 obj.setProperty('background_url' ,String(bg.url));
369 obj.setProperty('background_name',String(bg.name));
370 try { obj.flush(); } catch (e:Error) {}