Support editor-imagery-index
[potlatch2.git] / net / systemeD / potlatch2 / collections / Imagery.as
1 package net.systemeD.potlatch2.collections {
2
3         import flash.events.*;
4         import flash.display.*;
5         import flash.net.*;
6         import flash.text.TextField;
7         import net.systemeD.halcyon.FileBank;
8         import net.systemeD.halcyon.Map;
9         import net.systemeD.halcyon.MapEvent;
10         import net.systemeD.potlatch2.FunctionKeyManager;
11         import mx.collections.ArrayCollection;
12     import com.adobe.serialization.json.JSON;
13
14         /*
15                 There's lots of further tidying we can do:
16                 - remove the backreferences to _map and send events instead
17                 but this will do for now and help remove the clutter from potlatch2.mxml.
18         */
19
20         public class Imagery extends EventDispatcher {
21
22         private static const GLOBAL_INSTANCE:Imagery = new Imagery();
23         public static function instance():Imagery { return GLOBAL_INSTANCE; }
24
25                 private static const INDEX_URL:String="http://osmlab.github.io/editor-imagery-index/imagery.json";
26
27                 public var collection:Array=[];
28                 private var _selected:Object={};
29
30                 private var _map:Map;
31                 private var _overlay:Sprite;
32
33                 /* Load catalogue file */
34
35                 public function init(map:Map, overlay:Sprite):void {
36                         _map = map;
37                         _overlay = overlay;
38
39                         // load imagery file
40                         var request:URLRequest = new URLRequest(INDEX_URL);
41                         var loader:URLLoader = new URLLoader();
42                         loader.addEventListener(Event.COMPLETE, onImageryIndexLoad);
43                         loader.addEventListener(IOErrorEvent.IO_ERROR, onError);
44                         loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onError);
45                         loader.load(request);
46
47                         // create map listeners
48                         map.addEventListener(MapEvent.MOVE_END, moveHandler);
49                         map.addEventListener(MapEvent.RESIZE, resizeHandler);
50                 }
51
52                 private function onImageryIndexLoad(event:Event):void {
53                         var result:String = String(event.target.data);
54                         collection = com.adobe.serialization.json.JSON.decode(result) as Array;
55
56                         // Has the user saved something? If so, create dummy object
57                         var saved:Object = {};
58                         var bg:Object;
59                         if (SharedObject.getLocal("user_state","/").data['background_url']!=undefined) {
60                                 saved={ url:  SharedObject.getLocal("user_state","/").data['background_url' ],
61                                                 name: SharedObject.getLocal("user_state","/").data['background_name'],
62                                                 type: "tms",
63                                                 extent: { bbox: { min_lon: -180, max_lon: 180, min_lat: -90, max_lat: 90 } }}
64                         }
65
66                         var isSet:Boolean=false;
67             var backgroundSet:Boolean = false;
68                         collection.unshift({ name: "None", url: "" });
69
70                         // Is a set already chosen? (default to Bing if not)
71                         _selected=null;
72                         collection.forEach(function(bg:Object, index:int, array:Array):void {
73                                 if (saved.name && saved.name==bg.name) { _selected=bg; }
74                                 if (bg.id=='Bing') {
75                                         bg.url="http://ecn.t{switch:0,1,2,3}.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=587&mkt=en-gb&n=z";
76                                         bg.attribution={
77                                                 data_url: "http://dev.virtualearth.net/REST/v1/Imagery/Metadata/Aerial/0,0?zl=1&mapVersion=v1&key=Arzdiw4nlOJzRwOz__qailc8NiR31Tt51dN2D7cm57NrnceZnCpgOkmJhNpGoppU&include=ImageryProviders&output=xml",
78                                                 logo: "bing_maps.png",
79                                                 url: "http://opengeodata.org/microsoft-imagery-details"
80                                         }
81                                 }
82                                 if (bg.id=='Bing' && !_selected) { _selected=bg; }
83                                 if (bg.attribution && bg.attribution.logo) {
84                                         // load the logo (pretty much Bing-only)
85                                         FileBank.getInstance().addFromFile(bg.attribution.logo, function (fb:FileBank, name:String):void {
86                                                 bg.logoData   = fb.getAsBitmapData(name);
87                                                 bg.logoWidth  = fb.getWidth(name);
88                                                 bg.logoHeight = fb.getHeight(name);
89                                                 setLogo();
90                                                 });
91                                 }
92                                 if (bg.attribution && bg.attribution.data_url) {
93                                         // load the attribution (pretty much Bing-only)
94                                 var urlloader:URLLoader = new URLLoader();
95                                         urlloader.addEventListener(Event.COMPLETE, function(e:Event):void { onAttributionLoad(e,bg); });
96                                         urlloader.addEventListener(IOErrorEvent.IO_ERROR, onError);
97                                         urlloader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onError);
98                                 urlloader.load(new URLRequest(bg.attribution.data_url));
99                                 }
100                         });
101                         if (saved.name && !_selected) { collection.push(saved); _selected=saved; }
102                         setBackground(_selected);
103
104                         // Tell the function key manager that we'd like to receive function key calls
105                         FunctionKeyManager.instance().registerListener('Background imagery',
106                                 function(o:String):void { setBackground(findBackgroundWithName(o)); });
107                         dispatchEvent(new Event("collection_changed"));
108                 }
109                 
110                 private function onError(e:Event):void {
111                         // placeholder error routine so exception isn't thrown
112                 }
113                 
114                 public function onAttributionLoad(e:Event,bg: Object):void {
115                         // if we ever need to cope with non-Microsoft attribution, then this should look at bg.scheme
116             default xml namespace = Namespace("http://schemas.microsoft.com/search/local/ws/rest/v1");
117             var xml:XML = new XML(e.target.data);
118                         var providers:Object = {};
119             for each (var ImageryProvider:XML in xml..ImageryProvider) {
120                 var areas:Array=[];
121                 for each (var CoverageArea:XML in ImageryProvider.CoverageArea) {
122                     areas.push([CoverageArea.ZoomMin,
123                                 CoverageArea.ZoomMax,
124                                 CoverageArea.BoundingBox.SouthLatitude,
125                                 CoverageArea.BoundingBox.WestLongitude,
126                                 CoverageArea.BoundingBox.NorthLatitude,
127                                 CoverageArea.BoundingBox.EastLongitude]);
128                 }
129                 providers[ImageryProvider.Attribution]=areas;
130             }
131                         default xml namespace = new Namespace("");
132                         bg.attribution.providers=providers;
133                         setAttribution();
134                 }
135
136                 public function setBackground(bg:Object):void {
137                         // set background
138                         _selected=bg;
139                         dispatchEvent(new CollectionEvent(CollectionEvent.SELECT, bg));
140                         // update attribution and logo
141                         _overlay.visible=bg.hasOwnProperty('attribution');
142                         setLogo(); setAttribution(); setTerms();
143                         // save as SharedObject for next time
144                         var obj:SharedObject = SharedObject.getLocal("user_state","/");
145                         obj.setProperty('background_url' ,String(bg.url));
146                         obj.setProperty('background_name',String(bg.name));
147                         try { obj.flush(); } catch (e:Error) {}
148                 }
149                 
150                 public function get selected():Object { return _selected; }
151                 
152                 private function findBackgroundWithName(name:String):Object {
153                         for each (var bg:Object in collection) {
154                                 if (bg.name==name) { return bg; }
155                         }
156                         return { url:'' };
157                 }
158
159                 private function moveHandler(event:MapEvent):void {
160                         setAttribution();
161                         dispatchEvent(new Event("collection_changed"));
162                 }
163
164                 /* --------------------
165                    Attribution and logo */
166
167                 private function setAttribution():void {
168                         var tf:TextField=TextField(_overlay.getChildAt(0));
169                         tf.text='';
170                         if (!_selected.attribution) return;
171                         var attr:Array=[];
172                         if (_selected.attribution.providers) {
173                                 // Bing attribution scheme
174                                 for (var provider:String in _selected.attribution.providers) {
175                                         for each (var bounds:Array in _selected.attribution.providers[provider]) {
176                                                 if (_map.scale>=bounds[0] && _map.scale<=bounds[1] &&
177                                                   ((_map.edge_l>bounds[3] && _map.edge_l<bounds[5]) ||
178                                                    (_map.edge_r>bounds[3] && _map.edge_r<bounds[5]) ||
179                                            (_map.edge_l<bounds[3] && _map.edge_r>bounds[5])) &&
180                                                   ((_map.edge_b>bounds[2] && _map.edge_b<bounds[4]) ||
181                                                    (_map.edge_t>bounds[2] && _map.edge_t<bounds[4]) ||
182                                                    (_map.edge_b<bounds[2] && _map.edge_t>bounds[4]))) {
183                                                         attr.push(provider);
184                                                 }
185                                         }
186                                 }
187                         }
188                         if (attr.length==0) return;
189                         tf.text="Background "+attr.join(", ");
190                         positionAttribution();
191                         dispatchEvent(new MapEvent(MapEvent.BUMP, { y: tf.textHeight }));       // don't let the toolbox obscure it
192                 }
193                 private function positionAttribution():void {
194                         var tf:TextField=TextField(_overlay.getChildAt(0));
195                         tf.x=_map.mapwidth  - 5 - tf.textWidth;
196                         tf.y=_map.mapheight - 5 - tf.textHeight;
197                 }
198
199                 private function setLogo():void {
200                         while (_overlay.numChildren>2) { _overlay.removeChildAt(2); }
201                         if (!_selected.logoData) return;
202                         var logo:Sprite=new Sprite();
203                         logo.addChild(new Bitmap(_selected.logoData));
204                         if (_selected.attribution.url) { logo.buttonMode=true; logo.addEventListener(MouseEvent.CLICK, launchLogoLink, false, 0, true); }
205                         _overlay.addChild(logo);
206                         positionLogo();
207                 }
208                 private function positionLogo():void {
209                         _overlay.getChildAt(2).x=5;
210                         _overlay.getChildAt(2).y=_map.mapheight - 5 - _selected.logoHeight - (_selected.terms_url ? 10 : 0);
211                 }
212                 private function launchLogoLink(e:Event):void {
213                         if (!_selected.attribution.url) return;
214                         navigateToURL(new URLRequest(_selected.attribution.url), '_blank');
215                 }
216                 private function setTerms():void {
217                         var terms:TextField=TextField(_overlay.getChildAt(1));
218                         if (!_selected.attribution) { terms.text=''; return; }
219                         if (_selected.attribution && _selected.attribution.text) { terms.text=_selected.attribution.text; }
220                         else { terms.text="Background terms of use"; }
221                         positionTerms();
222                         terms.addEventListener(MouseEvent.CLICK, launchTermsLink, false, 0, true);
223                 }
224                 private function positionTerms():void {
225                         _overlay.getChildAt(1).x=5;
226                         _overlay.getChildAt(1).y=_map.mapheight - 15;
227                 }
228                 private function launchTermsLink(e:Event):void {
229                         if (!_selected.attribution.url) return;
230                         navigateToURL(new URLRequest(_selected.attribution.url), '_blank');
231                 }
232
233                 private function resizeHandler(event:MapEvent):void {
234                         if (_selected.logoData) positionLogo();
235                         if (_selected.terms_url) positionTerms();
236                         if (_selected.attribution) positionAttribution();
237                 }
238
239         [Bindable(event="collection_changed")]
240         public function getCollection():ArrayCollection {
241             return new ArrayCollection(collection);
242         }
243
244                 /* --------------------
245                    Imagery index parser */
246
247                 [Bindable(event="collection_changed")]
248                 public function getAvailableImagery():ArrayCollection {
249                         var available:Array=[];
250                         for each (var bg:Object in collection) {
251                                 if (bg.extent && bg.extent.polygon) {
252                                         // check if in boundary polygon
253                                         var included:Boolean=false;
254                                         for each (var poly:Array in bg.extent.polygon) {
255                                                 if (pointInPolygon(_map.centre_lon, _map.centre_lat, poly)) { included=true; }
256                                         }
257                                         if (included) { available.push(bg); }
258                                 } else if (bg.extent && bg.extent.bbox && bg.extent.bbox.min_lon) {
259                                         // if there's a bbox, check the current viewport intersects it
260                                         if (((_map.edge_l>bg.extent.bbox.min_lon && _map.edge_l<bg.extent.bbox.max_lon) ||
261                                              (_map.edge_r>bg.extent.bbox.min_lon && _map.edge_r<bg.extent.bbox.max_lon) ||
262                                              (_map.edge_l<bg.extent.bbox.min_lon && _map.edge_r>bg.extent.bbox.max_lon)) &&
263                                             ((_map.edge_b>bg.extent.bbox.min_lat && _map.edge_b<bg.extent.bbox.max_lat) ||
264                                              (_map.edge_t>bg.extent.bbox.min_lat && _map.edge_t<bg.extent.bbox.max_lat) ||
265                                              (_map.edge_b<bg.extent.bbox.min_lat && _map.edge_t>bg.extent.bbox.max_lat))) {
266                                                 available.push(bg);
267                                         }
268                                 } else if (!bg.type || bg.type!='wms') {
269                                         // if there's no bbox (i.e. global set) and default is set, include it
270                                         if (bg.name=='None' || bg.default) { available.push(bg); }
271                                 }
272                         }
273                         available.sort(function(a:Object,b:Object):int {
274                                 if (a.name=='None') { return -1; }
275                                 else if (b.name=='None') { return 1; }
276                                 else if (a.name<b.name) { return -1; }
277                                 else if (a.name>b.name) { return 1; }
278                                 return 0;
279                         });
280                         return new ArrayCollection(available);
281                 }
282
283                 public function pointInPolygon(x:Number,y:Number,vertices:Array):Boolean {
284                         // http://muongames.com/2013/07/point-in-a-polygon-in-as3-theory-and-code/
285                         // Loop through vertices, check if point is left of each line.
286                         // If it is, check if it line intersects with horizontal ray from point p
287                         var n:int = vertices.length;
288                         var j:int;
289                         var v1:Array, v2:Array;
290                         var count:int;
291                         for (var i:int=0; i<n; i++) {
292                                 j = i+1 == n ? 0 : i + 1;
293                                 v1 = vertices[i];
294                                 v2 = vertices[j];
295                                 // does point lie to the left of the line?
296                                 if (isLeft(x,y,v1,v2)) {
297                                         if ((y > v1[1] && y <= v2[1]) || (y > v2[1] && y <= v1[1])) { count++; }
298                                 }
299                         }
300                         return (count % 2 == 1);
301                 }
302
303                 public function isLeft(x:Number, y:Number, v1:Array, v2:Array):Boolean {
304                         if (v1[0] == v2[0]) { return (x <= v1[0]); }
305                         var m:Number = (v2[1] - v1[1]) / (v2[0] - v1[0]);
306                         var x2:Number = (y - v1[1]) / m + v1[0];
307                         return (x <= x2);
308                 }
309
310
311         }
312         
313 }