From 1a1f24aca70a62aacdb01d36a158eed0d7c2a592 Mon Sep 17 00:00:00 2001 From: Richard Fairhurst Date: Wed, 6 Aug 2014 03:03:13 +0100 Subject: [PATCH] Support editor-imagery-index --- net/systemeD/halcyon/Map.as | 1 + net/systemeD/halcyon/MapEvent.as | 4 +- net/systemeD/halcyon/TileSet.as | 21 +- net/systemeD/potlatch2/collections/Imagery.as | 212 +++++++++++------- .../potlatch2/controller/ControllerState.as | 12 +- 5 files changed, 150 insertions(+), 100 deletions(-) diff --git a/net/systemeD/halcyon/Map.as b/net/systemeD/halcyon/Map.as index 6a6b1ae6..75b15c01 100644 --- a/net/systemeD/halcyon/Map.as +++ b/net/systemeD/halcyon/Map.as @@ -209,6 +209,7 @@ package net.systemeD.halcyon { updateCoords(getX(),getY()); this.dispatchEvent(new MapEvent(MapEvent.RESIZE, {width:w, height:h})); + this.dispatchEvent(new MapEvent(MapEvent.MOVE_END, {lat:centre_lat, lon:centre_lon, scale:scale, minlon:edge_l, maxlon:edge_r, minlat:edge_b, maxlat:edge_t})); if ( backdrop != null ) { backdrop.width=mapwidth; diff --git a/net/systemeD/halcyon/MapEvent.as b/net/systemeD/halcyon/MapEvent.as index 907d6cf5..e4897cd6 100644 --- a/net/systemeD/halcyon/MapEvent.as +++ b/net/systemeD/halcyon/MapEvent.as @@ -9,8 +9,10 @@ package net.systemeD.halcyon { public static const DOWNLOAD:String = "download"; /** Viewport size has changed. */ public static const RESIZE:String = "resize"; - /** Map has moved relative to viewport. */ + /** Map is being moved relative to viewport. */ public static const MOVE:String = "move"; + /** Map has finished being moved relative to viewport. */ + public static const MOVE_END:String = "move_end"; /** Zoom level has changed. */ public static const SCALE:String = "scale"; /** Background imagery has moved relative to map. */ diff --git a/net/systemeD/halcyon/TileSet.as b/net/systemeD/halcyon/TileSet.as index 50f0dc9b..8f81d7d3 100644 --- a/net/systemeD/halcyon/TileSet.as +++ b/net/systemeD/halcyon/TileSet.as @@ -20,11 +20,11 @@ package net.systemeD.halcyon { private var tiles:Object={}; // key is "z,x,y"; value "true" if queued, or reference to loader object if requested private var loadcount:int=0; // number of tiles fully downloaded private var baseurl:String; // e.g. http://npe.openstreetmap.org/$z/$x/$y.png - private var scheme:String; // 900913 or microsoft + private var scheme:String; // tms or bing public var blocks:Array; // array of regexes which are verboten private var count:Number=0; // counter incremented to provide a/b/c/d tile swapping - private static const ROUNDROBIN:RegExp =/\$\{([^}]+)\}/; + private static const ROUNDROBIN:RegExp =/\{switch\:([^}]+)\}/; private var map:Map; private const MAXTILESLOADED:uint=30; @@ -49,7 +49,7 @@ package net.systemeD.halcyon { */ public function init(params:Object, update:Boolean=false):void { baseurl=params.url; - scheme =params.scheme ? params.scheme : '900913'; + scheme =params.type ? params.type : 'tms'; loadcount=0; for (var tilename:String in tiles) { if (tiles[tilename] is Loader) tiles[tilename].unload(); @@ -168,7 +168,7 @@ package net.systemeD.halcyon { var tmsy:int=Math.pow(2,tz)-1-ty; switch (scheme.toLowerCase()) { - case 'microsoft': + case 'bing': var u:String=''; for (var zoom:uint=tz; zoom>0; zoom--) { var byte:uint=0; @@ -177,24 +177,23 @@ package net.systemeD.halcyon { if ((ty & mask)!=0) byte+=2; u+=String(byte); } - t=baseurl.replace('$quadkey',u); break; - - case 'tms': - t=baseurl.replace('$z',map.scale).replace('$x',tx).replace('$y',tmsy); - break; + t=baseurl.replace('{quadkey}',u); break; default: - if (baseurl.indexOf('$x')>-1) { + if (baseurl.indexOf('{x}')>-1) { + t=baseurl.replace('{zoom}',map.scale).replace('{x}',tx).replace('{y}',ty).replace('{-y}',tmsy); + } else if (baseurl.indexOf('$x')>-1) { t=baseurl.replace('$z',map.scale).replace('$x',tx).replace('$y',ty).replace('$-y',tmsy); } else { t=baseurl.replace('!',map.scale).replace('!',tx).replace('!',ty); } + // also, someone should invent yet another variable substitution scheme break; } var o:Object=new Object(); if ((o=ROUNDROBIN.exec(t))) { - var prefixes:Array=o[1].split('|'); + var prefixes:Array=o[1].split(','); var p:String = prefixes[count % prefixes.length]; t=t.replace(ROUNDROBIN,p); count++; diff --git a/net/systemeD/potlatch2/collections/Imagery.as b/net/systemeD/potlatch2/collections/Imagery.as index 376f56a5..533dd086 100644 --- a/net/systemeD/potlatch2/collections/Imagery.as +++ b/net/systemeD/potlatch2/collections/Imagery.as @@ -9,6 +9,7 @@ package net.systemeD.potlatch2.collections { import net.systemeD.halcyon.MapEvent; import net.systemeD.potlatch2.FunctionKeyManager; import mx.collections.ArrayCollection; + import com.adobe.serialization.json.JSON; /* There's lots of further tidying we can do: @@ -21,6 +22,8 @@ package net.systemeD.potlatch2.collections { private static const GLOBAL_INSTANCE:Imagery = new Imagery(); public static function instance():Imagery { return GLOBAL_INSTANCE; } + private static const INDEX_URL:String="http://osmlab.github.io/editor-imagery-index/imagery.json"; + public var collection:Array=[]; private var _selected:Object={}; @@ -34,79 +37,69 @@ package net.systemeD.potlatch2.collections { _overlay = overlay; // load imagery file - FileBank.getInstance().addFromFile("imagery.xml", onImageryLoad); + var request:URLRequest = new URLRequest(INDEX_URL); + var loader:URLLoader = new URLLoader(); + loader.addEventListener(Event.COMPLETE, onImageryIndexLoad); + loader.addEventListener(IOErrorEvent.IO_ERROR, onError); + loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onError); + loader.load(request); // create map listeners - map.addEventListener(MapEvent.MOVE, moveHandler); + map.addEventListener(MapEvent.MOVE_END, moveHandler); map.addEventListener(MapEvent.RESIZE, resizeHandler); } - private function onImageryLoad(fileBank:FileBank, filename:String):void { - var xml:XML = new XML(fileBank.getAsString(filename)); + private function onImageryIndexLoad(event:Event):void { + var result:String = String(event.target.data); + collection = com.adobe.serialization.json.JSON.decode(result) as Array; + + // Has the user saved something? If so, create dummy object var saved:Object = {}; var bg:Object; if (SharedObject.getLocal("user_state","/").data['background_url']!=undefined) { - saved={ name: SharedObject.getLocal("user_state","/").data['background_name'], - url: SharedObject.getLocal("user_state","/").data['background_url' ] }; + saved={ url: SharedObject.getLocal("user_state","/").data['background_url' ], + name: SharedObject.getLocal("user_state","/").data['background_name'], + type: "tms", + extent: { bbox: { min_lon: -180, max_lon: 180, min_lat: -90, max_lat: 90 } }} } var isSet:Boolean=false; var backgroundSet:Boolean = false; + collection.unshift({ name: "None", url: "" }); - // Read all values from XML file - collection=new Array({ name: "None", url: "" }); - for each(var set:XML in xml.set) { - var obj:Object={}; - var a:XML; - for each (a in set.@*) { obj[a.name().localName]=a.toString(); } - for each (a in set.* ) { obj[a.name() ]=a.toString(); } - collection.push(obj); - if ((saved.url && obj.url ==saved.url) || - (saved.name && obj.name==saved.name && obj.name!='Custom')) { isSet=true; } - } - - // Add user's previous preference (from SharedObject) if we didn't find it in the XML file - if (!isSet && saved.name && saved.url && saved.url!='') { - collection.push(saved); - isSet=true; - } - - // Automatically select the user's previous preference - var defaultBackground:Object=null; - for each (bg in collection) { - if (bg.name==saved.name || bg.url==saved.url) { - setBackground(bg); - backgroundSet = true; - } else if (bg.default) { - defaultBackground=bg; - } - } - - // Otherwise, set whatever's specified as default - if (!backgroundSet && defaultBackground) { - setBackground(defaultBackground); - } - - // Get any attribution and logo details + // Is a set already chosen? (default to Bing if not) + _selected=null; collection.forEach(function(bg:Object, index:int, array:Array):void { - if (bg.logo) { - // load the logo - FileBank.getInstance().addFromFile(bg.logo, function (fb:FileBank, name:String):void { - bg.logoData = fb.getAsBitmapData(name); - bg.logoWidth = fb.getWidth(name); - bg.logoHeight = fb.getHeight(name); - setLogo(); - }); + if (saved.name && saved.name==bg.name) { _selected=bg; } + if (bg.id=='Bing') { + bg.url="http://ecn.t{switch:0,1,2,3}.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=587&mkt=en-gb&n=z"; + bg.attribution={ + data_url: "http://dev.virtualearth.net/REST/v1/Imagery/Metadata/Aerial/0,0?zl=1&mapVersion=v1&key=Arzdiw4nlOJzRwOz__qailc8NiR31Tt51dN2D7cm57NrnceZnCpgOkmJhNpGoppU&include=ImageryProviders&output=xml", + logo: "bing_maps.png", + url: "http://opengeodata.org/microsoft-imagery-details" + } + } + if (bg.id=='Bing' && !_selected) { _selected=bg; } + if (bg.attribution && bg.attribution.logo) { + // load the logo (pretty much Bing-only) + FileBank.getInstance().addFromFile(bg.attribution.logo, function (fb:FileBank, name:String):void { + bg.logoData = fb.getAsBitmapData(name); + bg.logoWidth = fb.getWidth(name); + bg.logoHeight = fb.getHeight(name); + setLogo(); + }); } - if (bg.attribution_url) { - // load the attribution + if (bg.attribution && bg.attribution.data_url) { + // load the attribution (pretty much Bing-only) var urlloader:URLLoader = new URLLoader(); urlloader.addEventListener(Event.COMPLETE, function(e:Event):void { onAttributionLoad(e,bg); }); urlloader.addEventListener(IOErrorEvent.IO_ERROR, onError); urlloader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onError); - urlloader.load(new URLRequest(bg.attribution_url)); + urlloader.load(new URLRequest(bg.attribution.data_url)); } }); + if (saved.name && !_selected) { collection.push(saved); _selected=saved; } + setBackground(_selected); // Tell the function key manager that we'd like to receive function key calls FunctionKeyManager.instance().registerListener('Background imagery', @@ -122,7 +115,7 @@ package net.systemeD.potlatch2.collections { // if we ever need to cope with non-Microsoft attribution, then this should look at bg.scheme default xml namespace = Namespace("http://schemas.microsoft.com/search/local/ws/rest/v1"); var xml:XML = new XML(e.target.data); - var attribution:Object = {}; + var providers:Object = {}; for each (var ImageryProvider:XML in xml..ImageryProvider) { var areas:Array=[]; for each (var CoverageArea:XML in ImageryProvider.CoverageArea) { @@ -133,10 +126,10 @@ package net.systemeD.potlatch2.collections { CoverageArea.BoundingBox.NorthLatitude, CoverageArea.BoundingBox.EastLongitude]); } - attribution[ImageryProvider.Attribution]=areas; + providers[ImageryProvider.Attribution]=areas; } default xml namespace = new Namespace(""); - bg.attribution=attribution; + bg.attribution.providers=providers; setAttribution(); } @@ -145,7 +138,7 @@ package net.systemeD.potlatch2.collections { _selected=bg; dispatchEvent(new CollectionEvent(CollectionEvent.SELECT, bg)); // update attribution and logo - _overlay.visible=bg.attribution || bg.logo || bg.terms_url; + _overlay.visible=bg.hasOwnProperty('attribution'); setLogo(); setAttribution(); setTerms(); // save as SharedObject for next time var obj:SharedObject = SharedObject.getLocal("user_state","/"); @@ -167,21 +160,28 @@ package net.systemeD.potlatch2.collections { setAttribution(); dispatchEvent(new Event("collection_changed")); } + + /* -------------------- + Attribution and logo */ + private function setAttribution():void { var tf:TextField=TextField(_overlay.getChildAt(0)); tf.text=''; if (!_selected.attribution) return; var attr:Array=[]; - for (var provider:String in _selected.attribution) { - for each (var bounds:Array in _selected.attribution[provider]) { - if (_map.scale>=bounds[0] && _map.scale<=bounds[1] && - ((_map.edge_l>bounds[3] && _map.edge_lbounds[3] && _map.edge_rbounds[5])) && - ((_map.edge_b>bounds[2] && _map.edge_bbounds[2] && _map.edge_tbounds[4]))) { - attr.push(provider); + if (_selected.attribution.providers) { + // Bing attribution scheme + for (var provider:String in _selected.attribution.providers) { + for each (var bounds:Array in _selected.attribution.providers[provider]) { + if (_map.scale>=bounds[0] && _map.scale<=bounds[1] && + ((_map.edge_l>bounds[3] && _map.edge_lbounds[3] && _map.edge_rbounds[5])) && + ((_map.edge_b>bounds[2] && _map.edge_bbounds[2] && _map.edge_tbounds[4]))) { + attr.push(provider); + } } } } @@ -201,7 +201,7 @@ package net.systemeD.potlatch2.collections { if (!_selected.logoData) return; var logo:Sprite=new Sprite(); logo.addChild(new Bitmap(_selected.logoData)); - if (_selected.logo_url) { logo.buttonMode=true; logo.addEventListener(MouseEvent.CLICK, launchLogoLink, false, 0, true); } + if (_selected.attribution.url) { logo.buttonMode=true; logo.addEventListener(MouseEvent.CLICK, launchLogoLink, false, 0, true); } _overlay.addChild(logo); positionLogo(); } @@ -210,13 +210,14 @@ package net.systemeD.potlatch2.collections { _overlay.getChildAt(2).y=_map.mapheight - 5 - _selected.logoHeight - (_selected.terms_url ? 10 : 0); } private function launchLogoLink(e:Event):void { - if (!_selected.logo_url) return; - navigateToURL(new URLRequest(_selected.logo_url), '_blank'); + if (!_selected.attribution.url) return; + navigateToURL(new URLRequest(_selected.attribution.url), '_blank'); } private function setTerms():void { var terms:TextField=TextField(_overlay.getChildAt(1)); - if (!_selected.terms_url) { terms.text=''; return; } - terms.text="Background terms of use"; + if (!_selected.attribution) { terms.text=''; return; } + if (_selected.attribution && _selected.attribution.text) { terms.text=_selected.attribution.text; } + else { terms.text="Background terms of use"; } positionTerms(); terms.addEventListener(MouseEvent.CLICK, launchTermsLink, false, 0, true); } @@ -225,8 +226,8 @@ package net.systemeD.potlatch2.collections { _overlay.getChildAt(1).y=_map.mapheight - 15; } private function launchTermsLink(e:Event):void { - if (!_selected.terms_url) return; - navigateToURL(new URLRequest(_selected.terms_url), '_blank'); + if (!_selected.attribution.url) return; + navigateToURL(new URLRequest(_selected.attribution.url), '_blank'); } private function resizeHandler(event:MapEvent):void { @@ -240,28 +241,73 @@ package net.systemeD.potlatch2.collections { return new ArrayCollection(collection); } + /* -------------------- + Imagery index parser */ + [Bindable(event="collection_changed")] public function getAvailableImagery():ArrayCollection { var available:Array=[]; for each (var bg:Object in collection) { - if (bg.minlon) { + if (bg.extent && bg.extent.polygon) { + // check if in boundary polygon + var included:Boolean=false; + for each (var poly:Array in bg.extent.polygon) { + if (pointInPolygon(_map.centre_lon, _map.centre_lat, poly)) { included=true; } + } + if (included) { available.push(bg); } + } else if (bg.extent && bg.extent.bbox && bg.extent.bbox.min_lon) { // if there's a bbox, check the current viewport intersects it - if (((_map.edge_l>bg.minlon && _map.edge_lbg.minlon && _map.edge_rbg.maxlon)) && - ((_map.edge_b>bg.minlat && _map.edge_bbg.minlat && _map.edge_tbg.maxlat))) { + if (((_map.edge_l>bg.extent.bbox.min_lon && _map.edge_lbg.extent.bbox.min_lon && _map.edge_rbg.extent.bbox.max_lon)) && + ((_map.edge_b>bg.extent.bbox.min_lat && _map.edge_bbg.extent.bbox.min_lat && _map.edge_tbg.extent.bbox.max_lat))) { available.push(bg); } - } else { - // if there's no bbox (i.e. global set), include it anyway - available.push(bg); + } else if (!bg.type || bg.type!='wms') { + // if there's no bbox (i.e. global set) and default is set, include it + if (bg.name=='None' || bg.default) { available.push(bg); } } } + available.sort(function(a:Object,b:Object):int { + if (a.name=='None') { return -1; } + else if (b.name=='None') { return 1; } + else if (a.nameb.name) { return 1; } + return 0; + }); return new ArrayCollection(available); } + public function pointInPolygon(x:Number,y:Number,vertices:Array):Boolean { + // http://muongames.com/2013/07/point-in-a-polygon-in-as3-theory-and-code/ + // Loop through vertices, check if point is left of each line. + // If it is, check if it line intersects with horizontal ray from point p + var n:int = vertices.length; + var j:int; + var v1:Array, v2:Array; + var count:int; + for (var i:int=0; i v1[1] && y <= v2[1]) || (y > v2[1] && y <= v1[1])) { count++; } + } + } + return (count % 2 == 1); + } + + public function isLeft(x:Number, y:Number, v1:Array, v2:Array):Boolean { + if (v1[0] == v2[0]) { return (x <= v1[0]); } + var m:Number = (v2[1] - v1[1]) / (v2[0] - v1[0]); + var x2:Number = (y - v1[1]) / m + v1[0]; + return (x <= x2); + } + + } } diff --git a/net/systemeD/potlatch2/controller/ControllerState.as b/net/systemeD/potlatch2/controller/ControllerState.as index e073545e..d6aa0c2a 100644 --- a/net/systemeD/potlatch2/controller/ControllerState.as +++ b/net/systemeD/potlatch2/controller/ControllerState.as @@ -266,11 +266,13 @@ package net.systemeD.potlatch2.controller { /** Create an action to add "source=*" tag to current entity based on background imagery. This is a convenient shorthand for users. */ protected function setSourceTag():void { if (selectCount!=1) { return; } - if (Imagery.instance().selected && Imagery.instance().selected.sourcetag) { - if ("sourcekey" in Imagery.instance().selected) - firstSelected.setTag(Imagery.instance().selected.sourcekey,Imagery.instance().selected.sourcetag, MainUndoStack.getGlobalStack().addAction); - else - firstSelected.setTag('source',Imagery.instance().selected.sourcetag, MainUndoStack.getGlobalStack().addAction); + if (!Imagery.instance().selected) { return; } + var sourceTag:String = Imagery.instance().selected.sourcetag || Imagery.instance().selected.id || Imagery.instance().selected.name; + if (sourceTag=='None') { return; } + if ("sourcekey" in Imagery.instance().selected) { + firstSelected.setTag(Imagery.instance().selected.sourcekey, sourceTag, MainUndoStack.getGlobalStack().addAction); + } else { + firstSelected.setTag('source', sourceTag, MainUndoStack.getGlobalStack().addAction); } controller.updateSelectionUI(); } -- 2.31.1