]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/Mapstraction/js/mxn.core.js
Merge branch 'master' into mmn_fixes
[quix0rs-gnu-social.git] / plugins / Mapstraction / js / mxn.core.js
1 (function(){
2
3 /**
4  * @exports mxn.util.$m as $m
5  */
6 var $m = mxn.util.$m;
7
8 /**
9  * Initialise our provider. This function should only be called
10  * from within mapstraction code, not exposed as part of the API.
11  * @private
12  */
13 var init = function() {
14         this.invoker.go('init', [ this.currentElement, this.api ]);
15         this.applyOptions();
16 };
17
18 /**
19  * Mapstraction instantiates a map with some API choice into the HTML element given
20  * @name mxn.Mapstraction
21  * @constructor
22  * @param {String} element The HTML element to replace with a map
23  * @param {String} api The API to use, one of 'yahoo', 'microsoft', 'openstreetmap', 'multimap', 'map24', 'openlayers', 'mapquest'. If omitted, first loaded provider implementation is used.
24  * @param {Bool} debug optional parameter to turn on debug support - this uses alert panels for unsupported actions
25  * @exports Mapstraction as mxn.Mapstraction
26  */
27 var Mapstraction = mxn.Mapstraction = function(element, api, debug) {
28     if (!api){
29                 api = mxn.util.getAvailableProviders()[0];
30         }
31         this.api = api;
32         this.maps = {};
33         this.currentElement = $m(element);
34         this.eventListeners = [];
35         this.markers = [];
36         this.layers = [];
37         this.polylines = [];
38         this.images = [];
39         this.controls = [];
40         this.loaded = {};
41         this.onload = {};
42         this.element = element;
43
44         // option defaults
45         this.options = {
46                 enableScrollWheelZoom: false,
47                 enableDragging: true
48         };
49
50         this.addControlsArgs = {};
51
52         // set up our invoker for calling API methods
53         this.invoker = new mxn.Invoker(this, 'Mapstraction', function(){ return this.api; });
54
55         // Adding our events
56         mxn.addEvents(this, [
57
58                 /**
59                  * Map has loaded
60                  * @name mxn.Mapstraction#load
61                  * @event
62                  */
63                 'load',
64
65                 /**
66                  * Map is clicked {location: LatLonPoint}
67                  * @name mxn.Mapstraction#click
68                  * @event
69                  */
70                 'click',
71
72                 /**
73                  * Map is panned
74                  * @name mxn.Mapstraction#endPan
75                  * @event
76                  */
77                 'endPan',
78
79                 /**
80                  * Zoom is changed
81                  * @name mxn.Mapstraction#changeZoom
82                  * @event
83                  */
84                 'changeZoom',
85
86                 /**
87                  * Marker is removed {marker: Marker}
88                  * @name mxn.Mapstraction#markerAdded
89                  * @event
90                  */
91                 'markerAdded',
92
93                 /**
94                  * Marker is removed {marker: Marker}
95                  * @name mxn.Mapstraction#markerRemoved
96                  * @event
97                  */
98                 'markerRemoved',
99
100                 /**
101                  * Polyline is added {polyline: Polyline}
102                  * @name mxn.Mapstraction#polylineAdded
103                  * @event
104                  */
105                 'polylineAdded',
106
107                 /**
108                  * Polyline is removed {polyline: Polyline}
109                  * @name mxn.Mapstraction#polylineRemoved
110                  * @event
111                  */
112                 'polylineRemoved'
113         ]);
114
115         // finally initialize our proper API map
116         init.apply(this);
117 };
118
119 // Map type constants
120 Mapstraction.ROAD = 1;
121 Mapstraction.SATELLITE = 2;
122 Mapstraction.HYBRID = 3;
123
124 // methods that have no implementation in mapstraction core
125 mxn.addProxyMethods(Mapstraction, [
126         /**
127          * Adds a large map panning control and zoom buttons to the map
128          * @name mxn.Mapstraction#addLargeControls
129          * @function
130          */
131         'addLargeControls',
132
133         /**
134          * Adds a map type control to the map (streets, aerial imagery etc)
135          * @name mxn.Mapstraction#addMapTypeControls
136          * @function
137          */
138         'addMapTypeControls',
139
140         /**
141          * Adds a GeoRSS or KML overlay to the map
142          *  some flavors of GeoRSS and KML are not supported by some of the Map providers
143          * @name mxn.Mapstraction#addOverlay
144          * @function
145          * @param {String} url GeoRSS or KML feed URL
146          * @param {Boolean} autoCenterAndZoom Set true to auto center and zoom after the feed is loaded
147          */
148         'addOverlay',
149
150         /**
151          * Adds a small map panning control and zoom buttons to the map
152          * @name mxn.Mapstraction#addSmallControls
153          * @function
154          */
155         'addSmallControls',
156
157         /**
158          * Applies the current option settings
159          * @name mxn.Mapstraction#applyOptions
160          * @function
161          */
162         'applyOptions',
163
164         /**
165          * Gets the BoundingBox of the map
166          * @name mxn.Mapstraction#getBounds
167          * @function
168          * @returns {BoundingBox} The bounding box for the current map state
169          */
170         'getBounds',
171
172         /**
173          * Gets the central point of the map
174          * @name mxn.Mapstraction#getCenter
175          * @function
176          * @returns {LatLonPoint} The center point of the map
177          */
178         'getCenter',
179
180         /**
181          * Gets the imagery type for the map.
182          * The type can be one of:
183          *  mxn.Mapstraction.ROAD
184          *  mxn.Mapstraction.SATELLITE
185          *  mxn.Mapstraction.HYBRID
186          * @name mxn.Mapstraction#getMapType
187          * @function
188          * @returns {Number}
189          */
190         'getMapType',
191
192         /**
193          * Returns a ratio to turn distance into pixels based on current projection
194          * @name mxn.Mapstraction#getPixelRatio
195          * @function
196          * @returns {Float} ratio
197          */
198         'getPixelRatio',
199
200         /**
201          * Returns the zoom level of the map
202          * @name mxn.Mapstraction#getZoom
203          * @function
204          * @returns {Integer} The zoom level of the map
205          */
206         'getZoom',
207
208         /**
209          * Returns the best zoom level for bounds given
210          * @name mxn.Mapstraction#getZoomLevelForBoundingBox
211          * @function
212          * @param {BoundingBox} bbox The bounds to fit
213          * @returns {Integer} The closest zoom level that contains the bounding box
214          */
215         'getZoomLevelForBoundingBox',
216
217         /**
218          * Displays the coordinates of the cursor in the HTML element
219          * @name mxn.Mapstraction#mousePosition
220          * @function
221          * @param {String} element ID of the HTML element to display the coordinates in
222          */
223         'mousePosition',
224
225         /**
226          * Resize the current map to the specified width and height
227          * (since it is actually on a child div of the mapElement passed
228          * as argument to the Mapstraction constructor, the resizing of this
229          * mapElement may have no effect on the size of the actual map)
230          * @name mxn.Mapstraction#resizeTo
231          * @function
232          * @param {Integer} width The width the map should be.
233          * @param {Integer} height The width the map should be.
234          */
235         'resizeTo',
236
237         /**
238          * Sets the map to the appropriate location and zoom for a given BoundingBox
239          * @name mxn.Mapstraction#setBounds
240          * @function
241          * @param {BoundingBox} bounds The bounding box you want the map to show
242          */
243         'setBounds',
244
245         /**
246          * setCenter sets the central point of the map
247          * @name mxn.Mapstraction#setCenter
248          * @function
249          * @param {LatLonPoint} point The point at which to center the map
250          * @param {Object} options Optional parameters
251          * @param {Boolean} options.pan Whether the map should move to the locations using a pan or just jump straight there
252          */
253         'setCenter',
254
255         /**
256          * Centers the map to some place and zoom level
257          * @name mxn.Mapstraction#setCenterAndZoom
258          * @function
259          * @param {LatLonPoint} point Where the center of the map should be
260          * @param {Integer} zoom The zoom level where 0 is all the way out.
261          */
262         'setCenterAndZoom',
263
264         /**
265          * Sets the imagery type for the map
266          * The type can be one of:
267          *  mxn.Mapstraction.ROAD
268          *  mxn.Mapstraction.SATELLITE
269          *  mxn.Mapstraction.HYBRID
270          * @name mxn.Mapstraction#setMapType
271          * @function
272          * @param {Number} type
273          */
274         'setMapType',
275
276         /**
277          * Sets the zoom level for the map
278          * MS doesn't seem to do zoom=0, and Gg's sat goes closer than it's maps, and MS's sat goes closer than Y!'s
279          * TODO: Mapstraction.prototype.getZoomLevels or something.
280          * @name mxn.Mapstraction#setZoom
281          * @function
282          * @param {Number} zoom The (native to the map) level zoom the map to.
283          */
284         'setZoom',
285
286         /**
287          * Turns a Tile Layer on or off
288          * @name mxn.Mapstraction#toggleTileLayer
289          * @function
290          * @param {tile_url} url of the tile layer that was created.
291          */
292         'toggleTileLayer'
293 ]);
294
295 /**
296  * Sets the current options to those specified in oOpts and applies them
297  * @param {Object} oOpts Hash of options to set
298  */
299 Mapstraction.prototype.setOptions = function(oOpts){
300         mxn.util.merge(this.options, oOpts);
301         this.applyOptions();
302 };
303
304 /**
305  * Sets an option and applies it.
306  * @param {String} sOptName Option name
307  * @param vVal Option value
308  */
309 Mapstraction.prototype.setOption = function(sOptName, vVal){
310         this.options[sOptName] = vVal;
311         this.applyOptions();
312 };
313
314 /**
315  * Enable scroll wheel zooming
316  * @deprecated Use setOption instead.
317  */
318 Mapstraction.prototype.enableScrollWheelZoom = function() {
319         this.setOption('enableScrollWheelZoom', true);
320 };
321
322 /**
323  * Enable/disable dragging of the map
324  * @param {Boolean} on
325  * @deprecated Use setOption instead.
326  */
327 Mapstraction.prototype.dragging = function(on) {
328         this.setOption('enableDragging', on);
329 };
330
331 /**
332  * Change the current api on the fly
333  * @param {String} api The API to swap to
334  * @param element
335  */
336 Mapstraction.prototype.swap = function(element,api) {
337         if (this.api === api) {
338                 return;
339         }
340
341         var center = this.getCenter();
342         var zoom = this.getZoom();
343
344         this.currentElement.style.visibility = 'hidden';
345         this.currentElement.style.display = 'none';
346
347         this.currentElement = $m(element);
348         this.currentElement.style.visibility = 'visible';
349         this.currentElement.style.display = 'block';
350
351         this.api = api;
352
353         if (this.maps[this.api] === undefined) {
354                 init.apply(this);
355
356                 this.setCenterAndZoom(center,zoom);
357
358                 for (var i = 0; i < this.markers.length; i++) {
359                         this.addMarker(this.markers[i], true);
360                 }
361
362                 for (var j = 0; j < this.polylines.length; j++) {
363                         this.addPolyline( this.polylines[j], true);
364                 }
365         }
366         else {
367
368                 //sync the view
369                 this.setCenterAndZoom(center,zoom);
370
371                 //TODO synchronize the markers and polylines too
372                 // (any overlays created after api instantiation are not sync'd)
373         }
374
375         this.addControls(this.addControlsArgs);
376
377 };
378
379 /**
380  * Returns the loaded state of a Map Provider
381  * @param {String} api Optional API to query for. If not specified, returns state of the originally created API
382  */
383 Mapstraction.prototype.isLoaded = function(api){
384         if (api === null) {
385                 api = this.api;
386         }
387         return this.loaded[api];
388 };
389
390 /**
391  * Set the debugging on or off - shows alert panels for functions that don't exist in Mapstraction
392  * @param {Boolean} debug true to turn on debugging, false to turn it off
393  */
394 Mapstraction.prototype.setDebug = function(debug){
395         if(debug !== null) {
396                 this.debug = debug;
397         }
398         return this.debug;
399 };
400
401
402 /////////////////////////
403 //
404 // Event Handling
405 //
406 // FIXME need to consolidate some of these handlers...
407 //
408 ///////////////////////////
409
410 // Click handler attached to native API
411 Mapstraction.prototype.clickHandler = function(lat, lon, me) {
412         this.callEventListeners('click', {
413                 location: new LatLonPoint(lat, lon)
414         });
415 };
416
417 // Move and zoom handler attached to native API
418 Mapstraction.prototype.moveendHandler = function(me) {
419         this.callEventListeners('moveend', {});
420 };
421
422 /**
423  * Add a listener for an event.
424  * @param {String} type Event type to attach listener to
425  * @param {Function} func Callback function
426  * @param {Object} caller Callback object
427  */
428 Mapstraction.prototype.addEventListener = function() {
429         var listener = {};
430         listener.event_type = arguments[0];
431         listener.callback_function = arguments[1];
432
433         // added the calling object so we can retain scope of callback function
434         if(arguments.length == 3) {
435                 listener.back_compat_mode = false;
436                 listener.callback_object = arguments[2];
437         }
438         else {
439                 listener.back_compat_mode = true;
440                 listener.callback_object = null;
441         }
442         this.eventListeners.push(listener);
443 };
444
445 /**
446  * Call listeners for a particular event.
447  * @param {String} sEventType Call listeners of this event type
448  * @param {Object} oEventArgs Event args object to pass back to the callback
449  */
450 Mapstraction.prototype.callEventListeners = function(sEventType, oEventArgs) {
451         oEventArgs.source = this;
452         for(var i = 0; i < this.eventListeners.length; i++) {
453                 var evLi = this.eventListeners[i];
454                 if(evLi.event_type == sEventType) {
455                         // only two cases for this, click and move
456                         if(evLi.back_compat_mode) {
457                                 if(evLi.event_type == 'click') {
458                                         evLi.callback_function(oEventArgs.location);
459                                 }
460                                 else {
461                                         evLi.callback_function();
462                                 }
463                         }
464                         else {
465                                 var scope = evLi.callback_object || this;
466                                 evLi.callback_function.call(scope, oEventArgs);
467                         }
468                 }
469         }
470 };
471
472
473 ////////////////////
474 //
475 // map manipulation
476 //
477 /////////////////////
478
479
480 /**
481  * addControls adds controls to the map. You specify which controls to add in
482  * the associative array that is the only argument.
483  * addControls can be called multiple time, with different args, to dynamically change controls.
484  *
485  * args = {
486  *       pan:     true,
487  *       zoom:   'large' || 'small',
488  *       overview: true,
489  *       scale: true,
490  *       map_type: true,
491  * }
492  * @param {array} args Which controls to switch on
493  */
494 Mapstraction.prototype.addControls = function( args ) {
495         this.addControlsArgs = args;
496         this.invoker.go('addControls', arguments);
497 };
498
499 /**
500  * Adds a marker pin to the map
501  * @param {Marker} marker The marker to add
502  * @param {Boolean} old If true, doesn't add this marker to the markers array. Used by the "swap" method
503  */
504 Mapstraction.prototype.addMarker = function(marker, old) {
505         marker.mapstraction = this;
506         marker.api = this.api;
507         marker.location.api = this.api;
508         marker.map = this.maps[this.api];
509         var propMarker = this.invoker.go('addMarker', arguments);
510         marker.setChild(propMarker);
511         if (!old) {
512                 this.markers.push(marker);
513         }
514         this.markerAdded.fire({'marker': marker});
515 };
516
517 /**
518  * addMarkerWithData will addData to the marker, then add it to the map
519  * @param {Marker} marker The marker to add
520  * @param {Object} data A data has to add
521  */
522 Mapstraction.prototype.addMarkerWithData = function(marker, data) {
523         marker.addData(data);
524         this.addMarker(marker);
525 };
526
527 /**
528  * addPolylineWithData will addData to the polyline, then add it to the map
529  * @param {Polyline} polyline The polyline to add
530  * @param {Object} data A data has to add
531  */
532 Mapstraction.prototype.addPolylineWithData = function(polyline, data) {
533         polyline.addData(data);
534         this.addPolyline(polyline);
535 };
536
537 /**
538  * removeMarker removes a Marker from the map
539  * @param {Marker} marker The marker to remove
540  */
541 Mapstraction.prototype.removeMarker = function(marker) {
542         var current_marker;
543         for(var i = 0; i < this.markers.length; i++){
544                 current_marker = this.markers[i];
545                 if(marker == current_marker) {
546                         this.invoker.go('removeMarker', arguments);
547                         marker.onmap = false;
548                         this.markers.splice(i, 1);
549                         this.markerRemoved.fire({'marker': marker});
550                         break;
551                 }
552         }
553 };
554
555 /**
556  * removeAllMarkers removes all the Markers on a map
557  */
558 Mapstraction.prototype.removeAllMarkers = function() {
559         var current_marker;
560         while(this.markers.length > 0) {
561                 current_marker = this.markers.pop();
562                 this.invoker.go('removeMarker', [current_marker]);
563         }
564 };
565
566 /**
567  * Declutter the markers on the map, group together overlapping markers.
568  * @param {Object} opts Declutter options
569  */
570 Mapstraction.prototype.declutterMarkers = function(opts) {
571         if(this.loaded[this.api] === false) {
572                 var me = this;
573                 this.onload[this.api].push( function() {
574                         me.declutterMarkers(opts);
575                 } );
576                 return;
577         }
578
579         var map = this.maps[this.api];
580
581         switch(this.api)
582         {
583                 //      case 'yahoo':
584                 //
585                 //        break;
586                 //      case 'openstreetmap':
587                 //
588                 //        break;
589                 //      case 'microsoft':
590                 //
591                 //        break;
592                 //      case 'openlayers':
593                 //
594                 //        break;
595                 case 'multimap':
596                         /*
597                          * Multimap supports quite a lot of decluttering options such as whether
598                          * to use an accurate of fast declutter algorithm and what icon to use to
599                          * represent a cluster. Using all this would mean abstracting all the enums
600                          * etc so we're only implementing the group name function at the moment.
601                          */
602                         map.declutterGroup(opts.groupName);
603                         break;
604                 //      case 'mapquest':
605                 //
606                 //        break;
607                 //      case 'map24':
608                 //
609                 //        break;
610                 case '  dummy':
611                         break;
612                 default:
613                         if(this.debug) {
614                                 alert(this.api + ' not supported by Mapstraction.declutterMarkers');
615                         }
616         }
617 };
618
619 /**
620  * Add a polyline to the map
621  * @param {Polyline} polyline The Polyline to add to the map
622  * @param {Boolean} old If true replaces an existing Polyline
623  */
624 Mapstraction.prototype.addPolyline = function(polyline, old) {
625         polyline.api = this.api;
626         polyline.map = this.maps[this.api];
627         var propPoly = this.invoker.go('addPolyline', arguments);
628         polyline.setChild(propPoly);
629         if(!old) {
630                 this.polylines.push(polyline);
631         }
632         this.polylineAdded.fire({'polyline': polyline});
633 };
634
635 // Private remove implementation
636 var removePolylineImpl = function(polyline) {
637         this.invoker.go('removePolyline', arguments);
638         polyline.onmap = false;
639         this.polylineRemoved.fire({'polyline': polyline});
640 };
641
642 /**
643  * Remove the polyline from the map
644  * @param {Polyline} polyline The Polyline to remove from the map
645  */
646 Mapstraction.prototype.removePolyline = function(polyline) {
647         var current_polyline;
648         for(var i = 0; i < this.polylines.length; i++){
649                 current_polyline = this.polylines[i];
650                 if(polyline == current_polyline) {
651                         this.polylines.splice(i, 1);
652                         removePolylineImpl.call(this, polyline);
653                         break;
654                 }
655         }
656 };
657
658 /**
659  * Removes all polylines from the map
660  */
661 Mapstraction.prototype.removeAllPolylines = function() {
662         var current_polyline;
663         while(this.polylines.length > 0) {
664                 current_polyline = this.polylines.pop();
665                 removePolylineImpl.call(this, current_polyline);
666         }
667 };
668
669 /**
670  * autoCenterAndZoom sets the center and zoom of the map to the smallest bounding box
671  * containing all markers
672  */
673 Mapstraction.prototype.autoCenterAndZoom = function() {
674         var lat_max = -90;
675         var lat_min = 90;
676         var lon_max = -180;
677         var lon_min = 180;
678         var lat, lon;
679         var checkMinMax = function(){
680                 if (lat > lat_max) {
681                         lat_max = lat;
682                 }
683                 if (lat < lat_min) {
684                         lat_min = lat;
685                 }
686                 if (lon > lon_max) {
687                         lon_max = lon;
688                 }
689                 if (lon < lon_min) {
690                         lon_min = lon;
691                 }
692         };
693         for (var i = 0; i < this.markers.length; i++) {
694                 lat = this.markers[i].location.lat;
695                 lon = this.markers[i].location.lon;
696                 checkMinMax();
697         }
698         for(i = 0; i < this.polylines.length; i++) {
699                 for (var j = 0; j < this.polylines[i].points.length; j++) {
700                         lat = this.polylines[i].points[j].lat;
701                         lon = this.polylines[i].points[j].lon;
702                         checkMinMax();
703                 }
704         }
705         this.setBounds( new BoundingBox(lat_min, lon_min, lat_max, lon_max) );
706 };
707
708 /**
709  * centerAndZoomOnPoints sets the center and zoom of the map from an array of points
710  *
711  * This is useful if you don't want to have to add markers to the map
712  */
713 Mapstraction.prototype.centerAndZoomOnPoints = function(points) {
714         var bounds = new BoundingBox(points[0].lat,points[0].lon,points[0].lat,points[0].lon);
715
716         for (var i=1, len = points.length ; i<len; i++) {
717                 bounds.extend(points[i]);
718         }
719
720         this.setBounds(bounds);
721 };
722
723 /**
724  * Sets the center and zoom of the map to the smallest bounding box
725  * containing all visible markers and polylines
726  * will only include markers and polylines with an attribute of "visible"
727  */
728 Mapstraction.prototype.visibleCenterAndZoom = function() {
729         var lat_max = -90;
730         var lat_min = 90;
731         var lon_max = -180;
732         var lon_min = 180;
733         var lat, lon;
734         var checkMinMax = function(){
735                 if (lat > lat_max) {
736                         lat_max = lat;
737                 }
738                 if (lat < lat_min) {
739                         lat_min = lat;
740                 }
741                 if (lon > lon_max) {
742                         lon_max = lon;
743                 }
744                 if (lon < lon_min) {
745                         lon_min = lon;
746                 }
747         };
748         for (var i=0; i<this.markers.length; i++) {
749                 if (this.markers[i].getAttribute("visible")) {
750                         lat = this.markers[i].location.lat;
751                         lon = this.markers[i].location.lon;
752                         checkMinMax();
753                 }
754         }
755
756         for (i=0; i<this.polylines.length; i++){
757                 if (this.polylines[i].getAttribute("visible")) {
758                         for (j=0; j<this.polylines[i].points.length; j++) {
759                                 lat = this.polylines[i].points[j].lat;
760                                 lon = this.polylines[i].points[j].lon;
761                                 checkMinMax();
762                         }
763                 }
764         }
765
766         this.setBounds(new BoundingBox(lat_min, lon_min, lat_max, lon_max));
767 };
768
769 /**
770  * Automatically sets center and zoom level to show all polylines
771  * Takes into account radious of polyline
772  * @param {Int} radius
773  */
774 Mapstraction.prototype.polylineCenterAndZoom = function(radius) {
775         var lat_max = -90;
776         var lat_min = 90;
777         var lon_max = -180;
778         var lon_min = 180;
779
780         for (var i=0; i < mapstraction.polylines.length; i++)
781         {
782                 for (var j=0; j<mapstraction.polylines[i].points.length; j++)
783                 {
784                         lat = mapstraction.polylines[i].points[j].lat;
785                         lon = mapstraction.polylines[i].points[j].lon;
786
787                         latConv = lonConv = radius;
788
789                         if (radius > 0)
790                         {
791                                 latConv = (radius / mapstraction.polylines[i].points[j].latConv());
792                                 lonConv = (radius / mapstraction.polylines[i].points[j].lonConv());
793                         }
794
795                         if ((lat + latConv) > lat_max) {
796                                 lat_max = (lat + latConv);
797                         }
798                         if ((lat - latConv) < lat_min) {
799                                 lat_min = (lat - latConv);
800                         }
801                         if ((lon + lonConv) > lon_max) {
802                                 lon_max = (lon + lonConv);
803                         }
804                         if ((lon - lonConv) < lon_min) {
805                                 lon_min = (lon - lonConv);
806                         }
807                 }
808         }
809
810         this.setBounds(new BoundingBox(lat_min, lon_min, lat_max, lon_max));
811 };
812
813 /**
814  * addImageOverlay layers an georeferenced image over the map
815  * @param {id} unique DOM identifier
816  * @param {src} url of image
817  * @param {opacity} opacity 0-100
818  * @param {west} west boundary
819  * @param {south} south boundary
820  * @param {east} east boundary
821  * @param {north} north boundary
822  */
823 Mapstraction.prototype.addImageOverlay = function(id, src, opacity, west, south, east, north) {
824
825         var b = document.createElement("img");
826         b.style.display = 'block';
827         b.setAttribute('id',id);
828         b.setAttribute('src',src);
829         b.style.position = 'absolute';
830         b.style.zIndex = 1;
831         b.setAttribute('west',west);
832         b.setAttribute('south',south);
833         b.setAttribute('east',east);
834         b.setAttribute('north',north);
835
836         var oContext = {
837                 imgElm: b
838         };
839
840         this.invoker.go('addImageOverlay', arguments, { context: oContext });
841 };
842
843 Mapstraction.prototype.setImageOpacity = function(id, opacity) {
844         if (opacity < 0) {
845                 opacity = 0;
846         }
847         if (opacity >= 100) {
848                 opacity = 100;
849         }
850         var c = opacity / 100;
851         var d = document.getElementById(id);
852         if(typeof(d.style.filter)=='string'){
853                 d.style.filter='alpha(opacity:'+opacity+')';
854         }
855         if(typeof(d.style.KHTMLOpacity)=='string'){
856                 d.style.KHTMLOpacity=c;
857         }
858         if(typeof(d.style.MozOpacity)=='string'){
859                 d.style.MozOpacity=c;
860         }
861         if(typeof(d.style.opacity)=='string'){
862                 d.style.opacity=c;
863         }
864 };
865
866 Mapstraction.prototype.setImagePosition = function(id) {
867         var imgElement = document.getElementById(id);
868         var oContext = {
869                 latLng: {
870                         top: imgElement.getAttribute('north'),
871                         left: imgElement.getAttribute('west'),
872                         bottom: imgElement.getAttribute('south'),
873                         right: imgElement.getAttribute('east')
874                 },
875                 pixels: { top: 0, right: 0, bottom: 0, left: 0 }
876         };
877
878         this.invoker.go('setImagePosition', arguments, { context: oContext });
879
880         imgElement.style.top = oContext.pixels.top.toString() + 'px';
881         imgElement.style.left = oContext.pixels.left.toString() + 'px';
882         imgElement.style.width = (oContext.pixels.right - oContext.pixels.left).toString() + 'px';
883         imgElement.style.height = (oContext.pixels.bottom - oContext.pixels.top).toString() + 'px';
884 };
885
886 Mapstraction.prototype.addJSON = function(json) {
887         var features;
888         if (typeof(json) == "string") {
889                 features = eval('(' + json + ')');
890         } else {
891                 features = json;
892         }
893         features = features.features;
894         var map = this.maps[this.api];
895         var html = "";
896         var item;
897         var polyline;
898         var marker;
899         var markers = [];
900
901         if(features.type == "FeatureCollection") {
902                 this.addJSON(features.features);
903         }
904
905         for (var i = 0; i < features.length; i++) {
906                 item = features[i];
907                 switch(item.geometry.type) {
908                         case "Point":
909                                 html = "<strong>" + item.title + "</strong><p>" + item.description + "</p>";
910                                 marker = new Marker(new LatLonPoint(item.geometry.coordinates[1],item.geometry.coordinates[0]));
911                                 markers.push(marker);
912                                 this.addMarkerWithData(marker,{
913                                         infoBubble : html,
914                                         label : item.title,
915                                         date : "new Date(\""+item.date+"\")",
916                                         iconShadow : item.icon_shadow,
917                                         marker : item.id,
918                                         iconShadowSize : item.icon_shadow_size,
919                                         icon : "http://boston.openguides.org/markers/AQUA.png",
920                                         iconSize : item.icon_size,
921                                         category : item.source_id,
922                                         draggable : false,
923                                         hover : false
924                                 });
925                                 break;
926                         case "Polygon":
927                                 var points = [];
928                                 polyline = new Polyline(points);
929                                 mapstraction.addPolylineWithData(polyline,{
930                                         fillColor : item.poly_color,
931                                         date : "new Date(\""+item.date+"\")",
932                                         category : item.source_id,
933                                         width : item.line_width,
934                                         opacity : item.line_opacity,
935                                         color : item.line_color,
936                                         polygon : true
937                                 });
938                                 markers.push(polyline);
939                                 break;
940                         default:
941                 // console.log("Geometry: " + features.items[i].geometry.type);
942                 }
943         }
944         return markers;
945 };
946
947 /**
948  * Adds a Tile Layer to the map
949  *
950  * Requires providing a parameterized tile url. Use {Z}, {X}, and {Y} to specify where the parameters
951  *  should go in the URL.
952  *
953  * For example, the OpenStreetMap tiles are:
954  *  m.addTileLayer("http://tile.openstreetmap.org/{Z}/{X}/{Y}.png", 1.0, "OSM", 1, 19, true);
955  *
956  * @param {tile_url} template url of the tiles.
957  * @param {opacity} opacity of the tile layer - 0 is transparent, 1 is opaque. (default=0.6)
958  * @param {copyright_text} copyright text to use for the tile layer. (default=Mapstraction)
959  * @param {min_zoom} Minimum (furtherest out) zoom level that tiles are available (default=1)
960  * @param {max_zoom} Maximum (closest) zoom level that the tiles are available (default=18)
961  * @param {map_type} Should the tile layer be a selectable map type in the layers palette (default=false)
962  */
963 Mapstraction.prototype.addTileLayer = function(tile_url, opacity, copyright_text, min_zoom, max_zoom, map_type) {
964         if(!tile_url) {
965                 return;
966         }
967
968         this.tileLayers = this.tileLayers || [];
969         opacity = opacity || 0.6;
970         copyright_text = copyright_text || "Mapstraction";
971         min_zoom = min_zoom || 1;
972         max_zoom = max_zoom || 18;
973         map_type = map_type || false;
974
975         return this.invoker.go('addTileLayer', [ tile_url, opacity, copyright_text, min_zoom, max_zoom, map_type] );
976 };
977
978 /**
979  * addFilter adds a marker filter
980  * @param {field} name of attribute to filter on
981  * @param {operator} presently only "ge" or "le"
982  * @param {value} the value to compare against
983  */
984 Mapstraction.prototype.addFilter = function(field, operator, value) {
985         if (!this.filters) {
986                 this.filters = [];
987         }
988         this.filters.push( [field, operator, value] );
989 };
990
991 /**
992  * Remove the specified filter
993  * @param {Object} field
994  * @param {Object} operator
995  * @param {Object} value
996  */
997 Mapstraction.prototype.removeFilter = function(field, operator, value) {
998         if (!this.filters) {
999                 return;
1000         }
1001
1002         var del;
1003         for (var f=0; f<this.filters.length; f++) {
1004                 if (this.filters[f][0] == field &&
1005                         (! operator || (this.filters[f][1] == operator && this.filters[f][2] == value))) {
1006                         this.filters.splice(f,1);
1007                         f--; //array size decreased
1008                 }
1009         }
1010 };
1011
1012 /**
1013  * Delete the current filter if present; otherwise add it
1014  * @param {Object} field
1015  * @param {Object} operator
1016  * @param {Object} value
1017  */
1018 Mapstraction.prototype.toggleFilter = function(field, operator, value) {
1019         if (!this.filters) {
1020                 this.filters = [];
1021         }
1022
1023         var found = false;
1024         for (var f = 0; f < this.filters.length; f++) {
1025                 if (this.filters[f][0] == field && this.filters[f][1] == operator && this.filters[f][2] == value) {
1026                         this.filters.splice(f,1);
1027                         f--; //array size decreased
1028                         found = true;
1029                 }
1030         }
1031
1032         if (! found) {
1033                 this.addFilter(field, operator, value);
1034         }
1035 };
1036
1037 /**
1038  * removeAllFilters
1039  */
1040 Mapstraction.prototype.removeAllFilters = function() {
1041         this.filters = [];
1042 };
1043
1044 /**
1045  * doFilter executes all filters added since last call
1046  * Now supports a callback function for when a marker is shown or hidden
1047  * @param {Function} showCallback
1048  * @param {Function} hideCallback
1049  * @returns {Int} count of visible markers
1050  */
1051 Mapstraction.prototype.doFilter = function(showCallback, hideCallback) {
1052         var map = this.maps[this.api];
1053         var visibleCount = 0;
1054         var f;
1055         if (this.filters) {
1056                 switch (this.api) {
1057                         case 'multimap':
1058                                 /* TODO polylines aren't filtered in multimap */
1059                                 var mmfilters = [];
1060                                 for (f=0; f<this.filters.length; f++) {
1061                                         mmfilters.push( new MMSearchFilter( this.filters[f][0], this.filters[f][1], this.filters[f][2] ));
1062                                 }
1063                                 map.setMarkerFilters( mmfilters );
1064                                 map.redrawMap();
1065                                 break;
1066                         case '  dummy':
1067                                 break;
1068                         default:
1069                                 var vis;
1070                                 for (var m=0; m<this.markers.length; m++) {
1071                                         vis = true;
1072                                         for (f = 0; f < this.filters.length; f++) {
1073                                                 if (! this.applyFilter(this.markers[m], this.filters[f])) {
1074                                                         vis = false;
1075                                                 }
1076                                         }
1077                                         if (vis) {
1078                                                 visibleCount ++;
1079                                                 if (showCallback){
1080                                                         showCallback(this.markers[m]);
1081                                                 }
1082                                                 else {
1083                                                         this.markers[m].show();
1084                                                 }
1085                                         }
1086                                         else {
1087                                                 if (hideCallback){
1088                                                         hideCallback(this.markers[m]);
1089                                                 }
1090                                                 else {
1091                                                         this.markers[m].hide();
1092                                                 }
1093                                         }
1094
1095                                         this.markers[m].setAttribute("visible", vis);
1096                                 }
1097                                 break;
1098                 }
1099         }
1100         return visibleCount;
1101 };
1102
1103 Mapstraction.prototype.applyFilter = function(o, f) {
1104         var vis = true;
1105         switch (f[1]) {
1106                 case 'ge':
1107                         if (o.getAttribute( f[0] ) < f[2]) {
1108                                 vis = false;
1109                         }
1110                         break;
1111                 case 'le':
1112                         if (o.getAttribute( f[0] ) > f[2]) {
1113                                 vis = false;
1114                         }
1115                         break;
1116                 case 'eq':
1117                         if (o.getAttribute( f[0] ) == f[2]) {
1118                                 vis = false;
1119                         }
1120                         break;
1121         }
1122
1123         return vis;
1124 };
1125
1126 /**
1127  * getAttributeExtremes returns the minimum/maximum of "field" from all markers
1128  * @param {field} name of "field" to query
1129  * @returns {array} of minimum/maximum
1130  */
1131 Mapstraction.prototype.getAttributeExtremes = function(field) {
1132         var min;
1133         var max;
1134         for (var m=0; m<this.markers.length; m++) {
1135                 if (! min || min > this.markers[m].getAttribute(field)) {
1136                         min = this.markers[m].getAttribute(field);
1137                 }
1138                 if (! max || max < this.markers[m].getAttribute(field)) {
1139                         max = this.markers[m].getAttribute(field);
1140                 }
1141         }
1142         for (var p=0; m<this.polylines.length; m++) {
1143                 if (! min || min > this.polylines[p].getAttribute(field)) {
1144                         min = this.polylines[p].getAttribute(field);
1145                 }
1146                 if (! max || max < this.polylines[p].getAttribute(field)) {
1147                         max = this.polylines[p].getAttribute(field);
1148                 }
1149         }
1150
1151         return [min, max];
1152 };
1153
1154 /**
1155  * getMap returns the native map object that mapstraction is talking to
1156  * @returns the native map object mapstraction is using
1157  */
1158 Mapstraction.prototype.getMap = function() {
1159         // FIXME in an ideal world this shouldn't exist right?
1160         return this.maps[this.api];
1161 };
1162
1163
1164 //////////////////////////////
1165 //
1166 //   LatLonPoint
1167 //
1168 /////////////////////////////
1169
1170 /**
1171  * LatLonPoint is a point containing a latitude and longitude with helper methods
1172  * @name mxn.LatLonPoint
1173  * @constructor
1174  * @param {double} lat is the latitude
1175  * @param {double} lon is the longitude
1176  * @exports LatLonPoint as mxn.LatLonPoint
1177  */
1178 var LatLonPoint = mxn.LatLonPoint = function(lat, lon) {
1179         // TODO error if undefined?
1180         //  if (lat == undefined) alert('undefined lat');
1181         //  if (lon == undefined) alert('undefined lon');
1182         this.lat = lat;
1183         this.lon = lon;
1184         this.lng = lon; // lets be lon/lng agnostic
1185
1186         this.invoker = new mxn.Invoker(this, 'LatLonPoint');
1187 };
1188
1189 mxn.addProxyMethods(LatLonPoint, [
1190         'fromProprietary', 'toProprietary'
1191 ], true);
1192
1193 /**
1194  * toString returns a string represntation of a point
1195  * @returns a string like '51.23, -0.123'
1196  * @type String
1197  */
1198 LatLonPoint.prototype.toString = function() {
1199         return this.lat + ', ' + this.lon;
1200 };
1201
1202 /**
1203  * distance returns the distance in kilometers between two points
1204  * @param {LatLonPoint} otherPoint The other point to measure the distance from to this one
1205  * @returns the distance between the points in kilometers
1206  * @type double
1207  */
1208 LatLonPoint.prototype.distance = function(otherPoint) {
1209         // Uses Haversine formula from http://www.movable-type.co.uk
1210         var rads = Math.PI / 180;
1211         var diffLat = (this.lat-otherPoint.lat) * rads;
1212         var diffLon = (this.lon-otherPoint.lon) * rads;
1213         var a = Math.sin(diffLat / 2) * Math.sin(diffLat / 2) +
1214                 Math.cos(this.lat*rads) * Math.cos(otherPoint.lat*rads) *
1215                 Math.sin(diffLon/2) * Math.sin(diffLon/2);
1216         return 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)) * 6371; // Earth's mean radius in km
1217 };
1218
1219 /**
1220  * equals tests if this point is the same as some other one
1221  * @param {LatLonPoint} otherPoint The other point to test with
1222  * @returns true or false
1223  * @type boolean
1224  */
1225 LatLonPoint.prototype.equals = function(otherPoint) {
1226         return this.lat == otherPoint.lat && this.lon == otherPoint.lon;
1227 };
1228
1229 /**
1230  * Returns latitude conversion based on current projection
1231  * @returns {Float} conversion
1232  */
1233 LatLonPoint.prototype.latConv = function() {
1234         return this.distance(new LatLonPoint(this.lat + 0.1, this.lon))*10;
1235 };
1236
1237 /**
1238  * Returns longitude conversion based on current projection
1239  * @returns {Float} conversion
1240  */
1241 LatLonPoint.prototype.lonConv = function() {
1242         return this.distance(new LatLonPoint(this.lat, this.lon + 0.1))*10;
1243 };
1244
1245
1246 //////////////////////////
1247 //
1248 //  BoundingBox
1249 //
1250 //////////////////////////
1251
1252 /**
1253  * BoundingBox creates a new bounding box object
1254  * @name mxn.BoundingBox
1255  * @constructor
1256  * @param {double} swlat the latitude of the south-west point
1257  * @param {double} swlon the longitude of the south-west point
1258  * @param {double} nelat the latitude of the north-east point
1259  * @param {double} nelon the longitude of the north-east point
1260  * @exports BoundingBox as mxn.BoundingBox
1261  */
1262 var BoundingBox = mxn.BoundingBox = function(swlat, swlon, nelat, nelon) {
1263         //FIXME throw error if box bigger than world
1264         //alert('new bbox ' + swlat + ',' +  swlon + ',' +  nelat + ',' + nelon);
1265         this.sw = new LatLonPoint(swlat, swlon);
1266         this.ne = new LatLonPoint(nelat, nelon);
1267 };
1268
1269 /**
1270  * getSouthWest returns a LatLonPoint of the south-west point of the bounding box
1271  * @returns the south-west point of the bounding box
1272  * @type LatLonPoint
1273  */
1274 BoundingBox.prototype.getSouthWest = function() {
1275         return this.sw;
1276 };
1277
1278 /**
1279  * getNorthEast returns a LatLonPoint of the north-east point of the bounding box
1280  * @returns the north-east point of the bounding box
1281  * @type LatLonPoint
1282  */
1283 BoundingBox.prototype.getNorthEast = function() {
1284         return this.ne;
1285 };
1286
1287 /**
1288  * isEmpty finds if this bounding box has zero area
1289  * @returns whether the north-east and south-west points of the bounding box are the same point
1290  * @type boolean
1291  */
1292 BoundingBox.prototype.isEmpty = function() {
1293         return this.ne == this.sw; // is this right? FIXME
1294 };
1295
1296 /**
1297  * contains finds whether a given point is within a bounding box
1298  * @param {LatLonPoint} point the point to test with
1299  * @returns whether point is within this bounding box
1300  * @type boolean
1301  */
1302 BoundingBox.prototype.contains = function(point){
1303         return point.lat >= this.sw.lat && point.lat <= this.ne.lat && point.lon >= this.sw.lon && point.lon <= this.ne.lon;
1304 };
1305
1306 /**
1307  * toSpan returns a LatLonPoint with the lat and lon as the height and width of the bounding box
1308  * @returns a LatLonPoint containing the height and width of this bounding box
1309  * @type LatLonPoint
1310  */
1311 BoundingBox.prototype.toSpan = function() {
1312         return new LatLonPoint( Math.abs(this.sw.lat - this.ne.lat), Math.abs(this.sw.lon - this.ne.lon) );
1313 };
1314
1315 /**
1316  * extend extends the bounding box to include the new point
1317  */
1318 BoundingBox.prototype.extend = function(point) {
1319         if(this.sw.lat > point.lat) {
1320                 this.sw.lat = point.lat;
1321         }
1322         if(this.sw.lon > point.lon) {
1323                 this.sw.lon = point.lon;
1324         }
1325         if(this.ne.lat < point.lat) {
1326                 this.ne.lat = point.lat;
1327         }
1328         if(this.ne.lon < point.lon) {
1329                 this.ne.lon = point.lon;
1330         }
1331         return;
1332 };
1333
1334 //////////////////////////////
1335 //
1336 //  Marker
1337 //
1338 ///////////////////////////////
1339
1340 /**
1341  * Marker create's a new marker pin
1342  * @name mxn.Marker
1343  * @constructor
1344  * @param {LatLonPoint} point the point on the map where the marker should go
1345  * @exports Marker as mxn.Marker
1346  */
1347 var Marker = mxn.Marker = function(point) {
1348         this.api = null;
1349         this.location = point;
1350         this.onmap = false;
1351         this.proprietary_marker = false;
1352         this.attributes = [];
1353         this.invoker = new mxn.Invoker(this, 'Marker', function(){return this.api;});
1354         mxn.addEvents(this, [
1355                 'openInfoBubble',       // Info bubble opened
1356                 'closeInfoBubble',      // Info bubble closed
1357                 'click'                         // Marker clicked
1358         ]);
1359 };
1360
1361 mxn.addProxyMethods(Marker, [
1362         'fromProprietary',
1363         'hide',
1364         'openBubble',
1365         'show',
1366         'toProprietary',
1367         'update'
1368 ]);
1369
1370 Marker.prototype.setChild = function(some_proprietary_marker) {
1371         this.proprietary_marker = some_proprietary_marker;
1372         some_proprietary_marker.mapstraction_marker = this;
1373         this.onmap = true;
1374 };
1375
1376 Marker.prototype.setLabel = function(labelText) {
1377         this.labelText = labelText;
1378 };
1379
1380 /**
1381  * addData conviniently set a hash of options on a marker
1382  */
1383 Marker.prototype.addData = function(options){
1384         for(var sOptKey in options) {
1385                 if(options.hasOwnProperty(sOptKey)){
1386                         switch(sOptKey) {
1387                                 case 'label':
1388                                         this.setLabel(options.label);
1389                                         break;
1390                                 case 'infoBubble':
1391                                         this.setInfoBubble(options.infoBubble);
1392                                         break;
1393                                 case 'icon':
1394                                         if(options.iconSize && options.iconAnchor) {
1395                                                 this.setIcon(options.icon, options.iconSize, options.iconAnchor);
1396                                         }
1397                                         else if(options.iconSize) {
1398                                                 this.setIcon(options.icon, options.iconSize);
1399                                         }
1400                                         else {
1401                                                 this.setIcon(options.icon);
1402                                         }
1403                                         break;
1404                                 case 'iconShadow':
1405                                         if(options.iconShadowSize) {
1406                                                 this.setShadowIcon(options.iconShadow, [ options.iconShadowSize[0], options.iconShadowSize[1] ]);
1407                                         }
1408                                         else {
1409                                                 this.setIcon(options.iconShadow);
1410                                         }
1411                                         break;
1412                                 case 'infoDiv':
1413                                         this.setInfoDiv(options.infoDiv[0],options.infoDiv[1]);
1414                                         break;
1415                                 case 'draggable':
1416                                         this.setDraggable(options.draggable);
1417                                         break;
1418                                 case 'hover':
1419                                         this.setHover(options.hover);
1420                                         this.setHoverIcon(options.hoverIcon);
1421                                         break;
1422                                 case 'hoverIcon':
1423                                         this.setHoverIcon(options.hoverIcon);
1424                                         break;
1425                                 case 'openBubble':
1426                                         this.openBubble();
1427                                         break;
1428                                 case 'groupName':
1429                                         this.setGroupName(options.groupName);
1430                                         break;
1431                                 default:
1432                                         // don't have a specific action for this bit of
1433                                         // data so set a named attribute
1434                                         this.setAttribute(sOptKey, options[sOptKey]);
1435                                         break;
1436                         }
1437                 }
1438         }
1439 };
1440
1441 /**
1442  * setInfoBubble sets the html/text content for a bubble popup for a marker
1443  * @param {String} infoBubble the html/text you want displayed
1444  */
1445 Marker.prototype.setInfoBubble = function(infoBubble) {
1446         this.infoBubble = infoBubble;
1447 };
1448
1449 /**
1450  * setInfoDiv sets the text and the id of the div element where to the information
1451  *  useful for putting information in a div outside of the map
1452  * @param {String} infoDiv the html/text you want displayed
1453  * @param {String} div the element id to use for displaying the text/html
1454  */
1455 Marker.prototype.setInfoDiv = function(infoDiv,div){
1456         this.infoDiv = infoDiv;
1457         this.div = div;
1458 };
1459
1460 /**
1461  * setIcon sets the icon for a marker
1462  * @param {String} iconUrl The URL of the image you want to be the icon
1463  */
1464 Marker.prototype.setIcon = function(iconUrl, iconSize, iconAnchor) {
1465         this.iconUrl = iconUrl;
1466         if(iconSize) {
1467                 this.iconSize = iconSize;
1468         }
1469         if(iconAnchor) {
1470                 this.iconAnchor = iconAnchor;
1471         }
1472 };
1473
1474 /**
1475  * setIconSize sets the size of the icon for a marker
1476  * @param {String} iconSize The array size in pixels of the marker image
1477  */
1478 Marker.prototype.setIconSize = function(iconSize){
1479         if(iconSize) {
1480                 this.iconSize = iconSize;
1481         }
1482 };
1483
1484 /**
1485  * setIconAnchor sets the anchor point for a marker
1486  * @param {String} iconAnchor The array offset of the anchor point
1487  */
1488 Marker.prototype.setIconAnchor = function(iconAnchor){
1489         if(iconAnchor) {
1490                 this.iconAnchor = iconAnchor;
1491         }
1492 };
1493
1494 /**
1495  * setShadowIcon sets the icon for a marker
1496  * @param {String} iconUrl The URL of the image you want to be the icon
1497  */
1498 Marker.prototype.setShadowIcon = function(iconShadowUrl, iconShadowSize){
1499         this.iconShadowUrl = iconShadowUrl;
1500         if(iconShadowSize) {
1501                 this.iconShadowSize = iconShadowSize;
1502         }
1503 };
1504
1505 Marker.prototype.setHoverIcon = function(hoverIconUrl){
1506         this.hoverIconUrl = hoverIconUrl;
1507 };
1508
1509 /**
1510  * setDraggable sets the draggable state of the marker
1511  * @param {Bool} draggable set to true if marker should be draggable by the user
1512  */
1513 Marker.prototype.setDraggable = function(draggable) {
1514         this.draggable = draggable;
1515 };
1516
1517 /**
1518  * setHover sets that the marker info is displayed on hover
1519  * @param {Bool} hover set to true if marker should display info on hover
1520  */
1521 Marker.prototype.setHover = function(hover) {
1522         this.hover = hover;
1523 };
1524
1525 /**
1526  * Markers are grouped up by this name. declutterGroup makes use of this.
1527  */
1528 Marker.prototype.setGroupName = function(sGrpName) {
1529         this.groupName = sGrpName;
1530 };
1531
1532 /**
1533  * setAttribute: set an arbitrary key/value pair on a marker
1534  * @arg(String) key
1535  * @arg value
1536  */
1537 Marker.prototype.setAttribute = function(key,value) {
1538         this.attributes[key] = value;
1539 };
1540
1541 /**
1542  * getAttribute: gets the value of "key"
1543  * @arg(String) key
1544  * @returns value
1545  */
1546 Marker.prototype.getAttribute = function(key) {
1547         return this.attributes[key];
1548 };
1549
1550
1551 ///////////////
1552 // Polyline ///
1553 ///////////////
1554
1555 /**
1556  * Instantiates a new Polyline.
1557  * @name mxn.Polyline
1558  * @constructor
1559  * @param {Point[]} points Points that make up the Polyline.
1560  * @exports Polyline as mxn.Polyline
1561  */
1562 var Polyline = mxn.Polyline = function(points) {
1563         this.api = null;
1564         this.points = points;
1565         this.attributes = [];
1566         this.onmap = false;
1567         this.proprietary_polyline = false;
1568         this.pllID = "mspll-"+new Date().getTime()+'-'+(Math.floor(Math.random()*Math.pow(2,16)));
1569         this.invoker = new mxn.Invoker(this, 'Polyline', function(){return this.api;});
1570 };
1571
1572 mxn.addProxyMethods(Polyline, [
1573         'fromProprietary',
1574         'hide',
1575         'show',
1576         'toProprietary',
1577         'update'
1578 ]);
1579
1580 /**
1581  * addData conviniently set a hash of options on a polyline
1582  */
1583 Polyline.prototype.addData = function(options){
1584         for(var sOpt in options) {
1585                 if(options.hasOwnProperty(sOpt)){
1586                         switch(sOpt) {
1587                                 case 'color':
1588                                         this.setColor(options.color);
1589                                         break;
1590                                 case 'width':
1591                                         this.setWidth(options.width);
1592                                         break;
1593                                 case 'opacity':
1594                                         this.setOpacity(options.opacity);
1595                                         break;
1596                                 case 'closed':
1597                                         this.setClosed(options.closed);
1598                                         break;
1599                                 case 'fillColor':
1600                                         this.setFillColor(options.fillColor);
1601                                         break;
1602                                 default:
1603                                         this.setAttribute(sOpt, options[sOpt]);
1604                                         break;
1605                         }
1606                 }
1607         }
1608 };
1609
1610 Polyline.prototype.setChild = function(some_proprietary_polyline) {
1611         this.proprietary_polyline = some_proprietary_polyline;
1612         this.onmap = true;
1613 };
1614
1615 /**
1616  * in the form: #RRGGBB
1617  * Note map24 insists on upper case, so we convert it.
1618  */
1619 Polyline.prototype.setColor = function(color){
1620         this.color = (color.length==7 && color[0]=="#") ? color.toUpperCase() : color;
1621 };
1622
1623 /**
1624  * Stroke width of the polyline
1625  * @param {Integer} width
1626  */
1627 Polyline.prototype.setWidth = function(width){
1628         this.width = width;
1629 };
1630
1631 /**
1632  * A float between 0.0 and 1.0
1633  * @param {Float} opacity
1634  */
1635 Polyline.prototype.setOpacity = function(opacity){
1636         this.opacity = opacity;
1637 };
1638
1639 /**
1640  * Marks the polyline as a closed polygon
1641  * @param {Boolean} bClosed
1642  */
1643 Polyline.prototype.setClosed = function(bClosed){
1644         this.closed = bClosed;
1645 };
1646
1647 /**
1648  * Fill color for a closed polyline as HTML color value e.g. #RRGGBB
1649  * @param {String} sFillColor HTML color value #RRGGBB
1650  */
1651 Polyline.prototype.setFillColor = function(sFillColor) {
1652         this.fillColor = sFillColor;
1653 };
1654
1655
1656 /**
1657  * setAttribute: set an arbitrary key/value pair on a polyline
1658  * @arg(String) key
1659  * @arg value
1660  */
1661 Polyline.prototype.setAttribute = function(key,value) {
1662         this.attributes[key] = value;
1663 };
1664
1665 /**
1666  * getAttribute: gets the value of "key"
1667  * @arg(String) key
1668  * @returns value
1669  */
1670 Polyline.prototype.getAttribute = function(key) {
1671         return this.attributes[key];
1672 };
1673
1674 /**
1675  * Simplifies a polyline, averaging and reducing the points
1676  * @param {Integer} tolerance (1.0 is a good starting point)
1677  */
1678 Polyline.prototype.simplify = function(tolerance) {
1679         var reduced = [];
1680
1681         // First point
1682         reduced[0] = this.points[0];
1683
1684         var markerPoint = 0;
1685
1686         for (var i = 1; i < this.points.length-1; i++){
1687                 if (this.points[i].distance(this.points[markerPoint]) >= tolerance)
1688                 {
1689                         reduced[reduced.length] = this.points[i];
1690                         markerPoint = i;
1691                 }
1692         }
1693
1694         // Last point
1695         reduced[reduced.length] = this.points[this.points.length-1];
1696
1697         // Revert
1698         this.points = reduced;
1699 };
1700
1701 ///////////////
1702 // Radius    //
1703 ///////////////
1704
1705 /**
1706  * Creates a new radius object for drawing circles around a point, does a lot of initial calculation to increase load time
1707  * @returns a new Radius
1708  * @type Radius
1709  * @constructor
1710  * @classDescription Radius
1711  * @param {Object} Center LatLonPoint of the radius
1712  * @param {quality} Number of points that comprise the approximated circle (20 is a good starting point)
1713  */
1714 var Radius = mxn.Radius = function(center, quality) {
1715         this.center = center;
1716         var latConv = center.latConv();
1717         var lonConv = center.lonConv();
1718
1719         // Create Radian conversion constant
1720         var rad = Math.PI / 180;
1721         this.calcs = [];
1722
1723         for(var i = 0; i < 360; i += quality){
1724             this.calcs.push([Math.cos(i * rad) / latConv, Math.sin(i * rad) / lonConv]);
1725         }
1726 };
1727
1728 /**
1729  * Returns polyline of a circle around the point based on new radius
1730  * @param {Radius} radius
1731  * @param {Colour} colour
1732  * @returns {Polyline} Polyline
1733  */
1734 Radius.prototype.getPolyline = function(radius, colour) {
1735         var points = [];
1736
1737         for(var i = 0; i < this.calcs.length; i++){
1738                 var point = new LatLonPoint(
1739                         this.center.lat + (radius * this.calcs[i][0]),
1740                         this.center.lon + (radius * this.calcs[i][1])
1741                 );
1742                 points.push(point);
1743         }
1744
1745         // Add first point
1746         points.push(points[0]);
1747
1748         var line = new Polyline(points);
1749         line.setColor(colour);
1750
1751         return line;
1752 };
1753
1754
1755 })();