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