Support editor-imagery-index
authorRichard Fairhurst <richard@systemeD.net>
Wed, 6 Aug 2014 02:03:13 +0000 (03:03 +0100)
committerRichard Fairhurst <richard@systemeD.net>
Wed, 6 Aug 2014 02:03:13 +0000 (03:03 +0100)
net/systemeD/halcyon/Map.as
net/systemeD/halcyon/MapEvent.as
net/systemeD/halcyon/TileSet.as
net/systemeD/potlatch2/collections/Imagery.as
net/systemeD/potlatch2/controller/ControllerState.as

index 6a6b1ae..75b15c0 100644 (file)
@@ -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;
index 907d6cf..e4897cd 100644 (file)
@@ -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. */
index 50f0dc9..8f81d7d 100644 (file)
@@ -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++;
index 376f56a..533dd08 100644 (file)
@@ -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&amp;mkt=en-gb&amp;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_l<bounds[5]) ||
-                                          (_map.edge_r>bounds[3] && _map.edge_r<bounds[5]) ||
-                                  (_map.edge_l<bounds[3] && _map.edge_r>bounds[5])) &&
-                                         ((_map.edge_b>bounds[2] && _map.edge_b<bounds[4]) ||
-                                          (_map.edge_t>bounds[2] && _map.edge_t<bounds[4]) ||
-                                          (_map.edge_b<bounds[2] && _map.edge_t>bounds[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_l<bounds[5]) ||
+                                                  (_map.edge_r>bounds[3] && _map.edge_r<bounds[5]) ||
+                                          (_map.edge_l<bounds[3] && _map.edge_r>bounds[5])) &&
+                                                 ((_map.edge_b>bounds[2] && _map.edge_b<bounds[4]) ||
+                                                  (_map.edge_t>bounds[2] && _map.edge_t<bounds[4]) ||
+                                                  (_map.edge_b<bounds[2] && _map.edge_t>bounds[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_l<bg.maxlon) ||
-                                            (_map.edge_r>bg.minlon && _map.edge_r<bg.maxlon) ||
-                                            (_map.edge_l<bg.minlon && _map.edge_r>bg.maxlon)) &&
-                                           ((_map.edge_b>bg.minlat && _map.edge_b<bg.maxlat) ||
-                                            (_map.edge_t>bg.minlat && _map.edge_t<bg.maxlat) ||
-                                            (_map.edge_b<bg.minlat && _map.edge_t>bg.maxlat))) {
+                                       if (((_map.edge_l>bg.extent.bbox.min_lon && _map.edge_l<bg.extent.bbox.max_lon) ||
+                                            (_map.edge_r>bg.extent.bbox.min_lon && _map.edge_r<bg.extent.bbox.max_lon) ||
+                                            (_map.edge_l<bg.extent.bbox.min_lon && _map.edge_r>bg.extent.bbox.max_lon)) &&
+                                           ((_map.edge_b>bg.extent.bbox.min_lat && _map.edge_b<bg.extent.bbox.max_lat) ||
+                                            (_map.edge_t>bg.extent.bbox.min_lat && _map.edge_t<bg.extent.bbox.max_lat) ||
+                                            (_map.edge_b<bg.extent.bbox.min_lat && _map.edge_t>bg.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.name<b.name) { return -1; }
+                               else if (a.name>b.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<n; i++) {
+                               j = i+1 == n ? 0 : i + 1;
+                               v1 = vertices[i];
+                               v2 = vertices[j];
+                               // does point lie to the left of the line?
+                               if (isLeft(x,y,v1,v2)) {
+                                       if ((y > 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);
+               }
+
+
        }
        
 }
index e073545..d6aa0c2 100644 (file)
@@ -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();
                }