]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/Mapstraction/js/mxn.core.js
Merge branch '0.9.x' of git@gitorious.org:statusnet/mainline into 0.9.x
[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 'google', 'googlev3', '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 'google':
587                 //
588                 //        break;
589                 //      case 'openstreetmap':
590                 //
591                 //        break;
592                 //      case 'microsoft':
593                 //
594                 //        break;
595                 //      case 'openlayers':
596                 //
597                 //        break;
598                 case 'multimap':
599                         /*
600                          * Multimap supports quite a lot of decluttering options such as whether
601                          * to use an accurate of fast declutter algorithm and what icon to use to
602                          * represent a cluster. Using all this would mean abstracting all the enums
603                          * etc so we're only implementing the group name function at the moment.
604                          */
605                         map.declutterGroup(opts.groupName);
606                         break;
607                 //      case 'mapquest':
608                 //
609                 //        break;
610                 //      case 'map24':
611                 //
612                 //        break;
613                 case '  dummy':
614                         break;
615                 default:
616                         if(this.debug) {
617                                 alert(this.api + ' not supported by Mapstraction.declutterMarkers');
618                         }
619         }
620 };
621
622 /**
623  * Add a polyline to the map
624  * @param {Polyline} polyline The Polyline to add to the map
625  * @param {Boolean} old If true replaces an existing Polyline
626  */
627 Mapstraction.prototype.addPolyline = function(polyline, old) {
628         polyline.api = this.api;
629         polyline.map = this.maps[this.api];
630         var propPoly = this.invoker.go('addPolyline', arguments);
631         polyline.setChild(propPoly);
632         if(!old) {
633                 this.polylines.push(polyline);
634         }
635         this.polylineAdded.fire({'polyline': polyline});
636 };
637
638 // Private remove implementation
639 var removePolylineImpl = function(polyline) {
640         this.invoker.go('removePolyline', arguments);
641         polyline.onmap = false;
642         this.polylineRemoved.fire({'polyline': polyline});
643 };
644
645 /**
646  * Remove the polyline from the map
647  * @param {Polyline} polyline The Polyline to remove from the map
648  */
649 Mapstraction.prototype.removePolyline = function(polyline) {
650         var current_polyline;
651         for(var i = 0; i < this.polylines.length; i++){
652                 current_polyline = this.polylines[i];
653                 if(polyline == current_polyline) {
654                         this.polylines.splice(i, 1);
655                         removePolylineImpl.call(this, polyline);
656                         break;
657                 }
658         }
659 };
660
661 /**
662  * Removes all polylines from the map
663  */
664 Mapstraction.prototype.removeAllPolylines = function() {
665         var current_polyline;
666         while(this.polylines.length > 0) {
667                 current_polyline = this.polylines.pop();
668                 removePolylineImpl.call(this, current_polyline);
669         }
670 };
671
672 /**
673  * autoCenterAndZoom sets the center and zoom of the map to the smallest bounding box
674  * containing all markers
675  */
676 Mapstraction.prototype.autoCenterAndZoom = function() {
677         var lat_max = -90;
678         var lat_min = 90;
679         var lon_max = -180;
680         var lon_min = 180;
681         var lat, lon;
682         var checkMinMax = function(){
683                 if (lat > lat_max) {
684                         lat_max = lat;
685                 }
686                 if (lat < lat_min) {
687                         lat_min = lat;
688                 }
689                 if (lon > lon_max) {
690                         lon_max = lon;
691                 }
692                 if (lon < lon_min) {
693                         lon_min = lon;
694                 }
695         };
696         for (var i = 0; i < this.markers.length; i++) {
697                 lat = this.markers[i].location.lat;
698                 lon = this.markers[i].location.lon;
699                 checkMinMax();
700         }
701         for(i = 0; i < this.polylines.length; i++) {
702                 for (var j = 0; j < this.polylines[i].points.length; j++) {
703                         lat = this.polylines[i].points[j].lat;
704                         lon = this.polylines[i].points[j].lon;
705                         checkMinMax();
706                 }
707         }
708         this.setBounds( new BoundingBox(lat_min, lon_min, lat_max, lon_max) );
709 };
710
711 /**
712  * centerAndZoomOnPoints sets the center and zoom of the map from an array of points
713  *
714  * This is useful if you don't want to have to add markers to the map
715  */
716 Mapstraction.prototype.centerAndZoomOnPoints = function(points) {
717         var bounds = new BoundingBox(points[0].lat,points[0].lon,points[0].lat,points[0].lon);
718
719         for (var i=1, len = points.length ; i<len; i++) {
720                 bounds.extend(points[i]);
721         }
722
723         this.setBounds(bounds);
724 };
725
726 /**
727  * Sets the center and zoom of the map to the smallest bounding box
728  * containing all visible markers and polylines
729  * will only include markers and polylines with an attribute of "visible"
730  */
731 Mapstraction.prototype.visibleCenterAndZoom = function() {
732         var lat_max = -90;
733         var lat_min = 90;
734         var lon_max = -180;
735         var lon_min = 180;
736         var lat, lon;
737         var checkMinMax = function(){
738                 if (lat > lat_max) {
739                         lat_max = lat;
740                 }
741                 if (lat < lat_min) {
742                         lat_min = lat;
743                 }
744                 if (lon > lon_max) {
745                         lon_max = lon;
746                 }
747                 if (lon < lon_min) {
748                         lon_min = lon;
749                 }
750         };
751         for (var i=0; i<this.markers.length; i++) {
752                 if (this.markers[i].getAttribute("visible")) {
753                         lat = this.markers[i].location.lat;
754                         lon = this.markers[i].location.lon;
755                         checkMinMax();
756                 }
757         }
758
759         for (i=0; i<this.polylines.length; i++){
760                 if (this.polylines[i].getAttribute("visible")) {
761                         for (j=0; j<this.polylines[i].points.length; j++) {
762                                 lat = this.polylines[i].points[j].lat;
763                                 lon = this.polylines[i].points[j].lon;
764                                 checkMinMax();
765                         }
766                 }
767         }
768
769         this.setBounds(new BoundingBox(lat_min, lon_min, lat_max, lon_max));
770 };
771
772 /**
773  * Automatically sets center and zoom level to show all polylines
774  * Takes into account radious of polyline
775  * @param {Int} radius
776  */
777 Mapstraction.prototype.polylineCenterAndZoom = function(radius) {
778         var lat_max = -90;
779         var lat_min = 90;
780         var lon_max = -180;
781         var lon_min = 180;
782
783         for (var i=0; i < mapstraction.polylines.length; i++)
784         {
785                 for (var j=0; j<mapstraction.polylines[i].points.length; j++)
786                 {
787                         lat = mapstraction.polylines[i].points[j].lat;
788                         lon = mapstraction.polylines[i].points[j].lon;
789
790                         latConv = lonConv = radius;
791
792                         if (radius > 0)
793                         {
794                                 latConv = (radius / mapstraction.polylines[i].points[j].latConv());
795                                 lonConv = (radius / mapstraction.polylines[i].points[j].lonConv());
796                         }
797
798                         if ((lat + latConv) > lat_max) {
799                                 lat_max = (lat + latConv);
800                         }
801                         if ((lat - latConv) < lat_min) {
802                                 lat_min = (lat - latConv);
803                         }
804                         if ((lon + lonConv) > lon_max) {
805                                 lon_max = (lon + lonConv);
806                         }
807                         if ((lon - lonConv) < lon_min) {
808                                 lon_min = (lon - lonConv);
809                         }
810                 }
811         }
812
813         this.setBounds(new BoundingBox(lat_min, lon_min, lat_max, lon_max));
814 };
815
816 /**
817  * addImageOverlay layers an georeferenced image over the map
818  * @param {id} unique DOM identifier
819  * @param {src} url of image
820  * @param {opacity} opacity 0-100
821  * @param {west} west boundary
822  * @param {south} south boundary
823  * @param {east} east boundary
824  * @param {north} north boundary
825  */
826 Mapstraction.prototype.addImageOverlay = function(id, src, opacity, west, south, east, north) {
827         
828         var b = document.createElement("img");
829         b.style.display = 'block';
830         b.setAttribute('id',id);
831         b.setAttribute('src',src);
832         b.style.position = 'absolute';
833         b.style.zIndex = 1;
834         b.setAttribute('west',west);
835         b.setAttribute('south',south);
836         b.setAttribute('east',east);
837         b.setAttribute('north',north);
838         
839         var oContext = {
840                 imgElm: b
841         };
842         
843         this.invoker.go('addImageOverlay', arguments, { context: oContext });
844 };
845
846 Mapstraction.prototype.setImageOpacity = function(id, opacity) {
847         if (opacity < 0) {
848                 opacity = 0;
849         }
850         if (opacity >= 100) {
851                 opacity = 100;
852         }
853         var c = opacity / 100;
854         var d = document.getElementById(id);
855         if(typeof(d.style.filter)=='string'){
856                 d.style.filter='alpha(opacity:'+opacity+')';
857         }
858         if(typeof(d.style.KHTMLOpacity)=='string'){
859                 d.style.KHTMLOpacity=c;
860         }
861         if(typeof(d.style.MozOpacity)=='string'){
862                 d.style.MozOpacity=c;
863         }
864         if(typeof(d.style.opacity)=='string'){
865                 d.style.opacity=c;
866         }
867 };
868
869 Mapstraction.prototype.setImagePosition = function(id) {
870         var imgElement = document.getElementById(id);
871         var oContext = {
872                 latLng: { 
873                         top: imgElement.getAttribute('north'),
874                         left: imgElement.getAttribute('west'),
875                         bottom: imgElement.getAttribute('south'),
876                         right: imgElement.getAttribute('east')
877                 },
878                 pixels: { top: 0, right: 0, bottom: 0, left: 0 }
879         };
880         
881         this.invoker.go('setImagePosition', arguments, { context: oContext });
882
883         imgElement.style.top = oContext.pixels.top.toString() + 'px';
884         imgElement.style.left = oContext.pixels.left.toString() + 'px';
885         imgElement.style.width = (oContext.pixels.right - oContext.pixels.left).toString() + 'px';
886         imgElement.style.height = (oContext.pixels.bottom - oContext.pixels.top).toString() + 'px';
887 };
888
889 Mapstraction.prototype.addJSON = function(json) {
890         var features;
891         if (typeof(json) == "string") {
892                 features = eval('(' + json + ')');
893         } else {
894                 features = json;
895         }
896         features = features.features;
897         var map = this.maps[this.api];
898         var html = "";
899         var item;
900         var polyline;
901         var marker;
902         var markers = [];
903
904         if(features.type == "FeatureCollection") {
905                 this.addJSON(features.features);
906         }
907
908         for (var i = 0; i < features.length; i++) {
909                 item = features[i];
910                 switch(item.geometry.type) {
911                         case "Point":
912                                 html = "<strong>" + item.title + "</strong><p>" + item.description + "</p>";
913                                 marker = new Marker(new LatLonPoint(item.geometry.coordinates[1],item.geometry.coordinates[0]));
914                                 markers.push(marker);
915                                 this.addMarkerWithData(marker,{
916                                         infoBubble : html,
917                                         label : item.title,
918                                         date : "new Date(\""+item.date+"\")",
919                                         iconShadow : item.icon_shadow,
920                                         marker : item.id,
921                                         iconShadowSize : item.icon_shadow_size,
922                                         icon : "http://boston.openguides.org/markers/AQUA.png",
923                                         iconSize : item.icon_size,
924                                         category : item.source_id,
925                                         draggable : false,
926                                         hover : false
927                                 });
928                                 break;
929                         case "Polygon":
930                                 var points = [];
931                                 polyline = new Polyline(points);
932                                 mapstraction.addPolylineWithData(polyline,{
933                                         fillColor : item.poly_color,
934                                         date : "new Date(\""+item.date+"\")",
935                                         category : item.source_id,
936                                         width : item.line_width,
937                                         opacity : item.line_opacity,
938                                         color : item.line_color,
939                                         polygon : true
940                                 });
941                                 markers.push(polyline);
942                                 break;
943                         default:
944                 // console.log("Geometry: " + features.items[i].geometry.type);
945                 }
946         }
947         return markers;
948 };
949
950 /**
951  * Adds a Tile Layer to the map
952  *
953  * Requires providing a parameterized tile url. Use {Z}, {X}, and {Y} to specify where the parameters
954  *  should go in the URL.
955  *
956  * For example, the OpenStreetMap tiles are:
957  *  m.addTileLayer("http://tile.openstreetmap.org/{Z}/{X}/{Y}.png", 1.0, "OSM", 1, 19, true);
958  *
959  * @param {tile_url} template url of the tiles.
960  * @param {opacity} opacity of the tile layer - 0 is transparent, 1 is opaque. (default=0.6)
961  * @param {copyright_text} copyright text to use for the tile layer. (default=Mapstraction)
962  * @param {min_zoom} Minimum (furtherest out) zoom level that tiles are available (default=1)
963  * @param {max_zoom} Maximum (closest) zoom level that the tiles are available (default=18)
964  * @param {map_type} Should the tile layer be a selectable map type in the layers palette (default=false)
965  */
966 Mapstraction.prototype.addTileLayer = function(tile_url, opacity, copyright_text, min_zoom, max_zoom, map_type) {
967         if(!tile_url) {
968                 return;
969         }
970         
971         this.tileLayers = this.tileLayers || [];        
972         opacity = opacity || 0.6;
973         copyright_text = copyright_text || "Mapstraction";
974         min_zoom = min_zoom || 1;
975         max_zoom = max_zoom || 18;
976         map_type = map_type || false;
977
978         return this.invoker.go('addTileLayer', [ tile_url, opacity, copyright_text, min_zoom, max_zoom, map_type] );
979 };
980
981 /**
982  * addFilter adds a marker filter
983  * @param {field} name of attribute to filter on
984  * @param {operator} presently only "ge" or "le"
985  * @param {value} the value to compare against
986  */
987 Mapstraction.prototype.addFilter = function(field, operator, value) {
988         if (!this.filters) {
989                 this.filters = [];
990         }
991         this.filters.push( [field, operator, value] );
992 };
993
994 /**
995  * Remove the specified filter
996  * @param {Object} field
997  * @param {Object} operator
998  * @param {Object} value
999  */
1000 Mapstraction.prototype.removeFilter = function(field, operator, value) {
1001         if (!this.filters) {
1002                 return;
1003         }
1004
1005         var del;
1006         for (var f=0; f<this.filters.length; f++) {
1007                 if (this.filters[f][0] == field &&
1008                         (! operator || (this.filters[f][1] == operator && this.filters[f][2] == value))) {
1009                         this.filters.splice(f,1);
1010                         f--; //array size decreased
1011                 }
1012         }
1013 };
1014
1015 /**
1016  * Delete the current filter if present; otherwise add it
1017  * @param {Object} field
1018  * @param {Object} operator
1019  * @param {Object} value
1020  */
1021 Mapstraction.prototype.toggleFilter = function(field, operator, value) {
1022         if (!this.filters) {
1023                 this.filters = [];
1024         }
1025
1026         var found = false;
1027         for (var f = 0; f < this.filters.length; f++) {
1028                 if (this.filters[f][0] == field && this.filters[f][1] == operator && this.filters[f][2] == value) {
1029                         this.filters.splice(f,1);
1030                         f--; //array size decreased
1031                         found = true;
1032                 }
1033         }
1034
1035         if (! found) {
1036                 this.addFilter(field, operator, value);
1037         }
1038 };
1039
1040 /**
1041  * removeAllFilters
1042  */
1043 Mapstraction.prototype.removeAllFilters = function() {
1044         this.filters = [];
1045 };
1046
1047 /**
1048  * doFilter executes all filters added since last call
1049  * Now supports a callback function for when a marker is shown or hidden
1050  * @param {Function} showCallback
1051  * @param {Function} hideCallback
1052  * @returns {Int} count of visible markers
1053  */
1054 Mapstraction.prototype.doFilter = function(showCallback, hideCallback) {
1055         var map = this.maps[this.api];
1056         var visibleCount = 0;
1057         var f;
1058         if (this.filters) {
1059                 switch (this.api) {
1060                         case 'multimap':
1061                                 /* TODO polylines aren't filtered in multimap */
1062                                 var mmfilters = [];
1063                                 for (f=0; f<this.filters.length; f++) {
1064                                         mmfilters.push( new MMSearchFilter( this.filters[f][0], this.filters[f][1], this.filters[f][2] ));
1065                                 }
1066                                 map.setMarkerFilters( mmfilters );
1067                                 map.redrawMap();
1068                                 break;
1069                         case '  dummy':
1070                                 break;
1071                         default:
1072                                 var vis;
1073                                 for (var m=0; m<this.markers.length; m++) {
1074                                         vis = true;
1075                                         for (f = 0; f < this.filters.length; f++) {
1076                                                 if (! this.applyFilter(this.markers[m], this.filters[f])) {
1077                                                         vis = false;
1078                                                 }
1079                                         }
1080                                         if (vis) {
1081                                                 visibleCount ++;
1082                                                 if (showCallback){
1083                                                         showCallback(this.markers[m]);
1084                                                 }
1085                                                 else {
1086                                                         this.markers[m].show();
1087                                                 }
1088                                         } 
1089                                         else { 
1090                                                 if (hideCallback){
1091                                                         hideCallback(this.markers[m]);
1092                                                 }
1093                                                 else {
1094                                                         this.markers[m].hide();
1095                                                 }
1096                                         }
1097
1098                                         this.markers[m].setAttribute("visible", vis);
1099                                 }
1100                                 break;
1101                 }
1102         }
1103         return visibleCount;
1104 };
1105
1106 Mapstraction.prototype.applyFilter = function(o, f) {
1107         var vis = true;
1108         switch (f[1]) {
1109                 case 'ge':
1110                         if (o.getAttribute( f[0] ) < f[2]) {
1111                                 vis = false;
1112                         }
1113                         break;
1114                 case 'le':
1115                         if (o.getAttribute( f[0] ) > f[2]) {
1116                                 vis = false;
1117                         }
1118                         break;
1119                 case 'eq':
1120                         if (o.getAttribute( f[0] ) == f[2]) {
1121                                 vis = false;
1122                         }
1123                         break;
1124         }
1125
1126         return vis;
1127 };
1128
1129 /**
1130  * getAttributeExtremes returns the minimum/maximum of "field" from all markers
1131  * @param {field} name of "field" to query
1132  * @returns {array} of minimum/maximum
1133  */
1134 Mapstraction.prototype.getAttributeExtremes = function(field) {
1135         var min;
1136         var max;
1137         for (var m=0; m<this.markers.length; m++) {
1138                 if (! min || min > this.markers[m].getAttribute(field)) {
1139                         min = this.markers[m].getAttribute(field);
1140                 }
1141                 if (! max || max < this.markers[m].getAttribute(field)) {
1142                         max = this.markers[m].getAttribute(field);
1143                 }
1144         }
1145         for (var p=0; m<this.polylines.length; m++) {
1146                 if (! min || min > this.polylines[p].getAttribute(field)) {
1147                         min = this.polylines[p].getAttribute(field);
1148                 }
1149                 if (! max || max < this.polylines[p].getAttribute(field)) {
1150                         max = this.polylines[p].getAttribute(field);
1151                 }
1152         }
1153
1154         return [min, max];
1155 };
1156
1157 /**
1158  * getMap returns the native map object that mapstraction is talking to
1159  * @returns the native map object mapstraction is using
1160  */
1161 Mapstraction.prototype.getMap = function() {
1162         // FIXME in an ideal world this shouldn't exist right?
1163         return this.maps[this.api];
1164 };
1165
1166
1167 //////////////////////////////
1168 //
1169 //   LatLonPoint
1170 //
1171 /////////////////////////////
1172
1173 /**
1174  * LatLonPoint is a point containing a latitude and longitude with helper methods
1175  * @name mxn.LatLonPoint
1176  * @constructor
1177  * @param {double} lat is the latitude
1178  * @param {double} lon is the longitude
1179  * @exports LatLonPoint as mxn.LatLonPoint
1180  */
1181 var LatLonPoint = mxn.LatLonPoint = function(lat, lon) {
1182         // TODO error if undefined?
1183         //  if (lat == undefined) alert('undefined lat');
1184         //  if (lon == undefined) alert('undefined lon');
1185         this.lat = lat;
1186         this.lon = lon;
1187         this.lng = lon; // lets be lon/lng agnostic
1188         
1189         this.invoker = new mxn.Invoker(this, 'LatLonPoint');            
1190 };
1191
1192 mxn.addProxyMethods(LatLonPoint, [ 
1193         'fromProprietary', 'toProprietary'
1194 ], true);
1195
1196 /**
1197  * toString returns a string represntation of a point
1198  * @returns a string like '51.23, -0.123'
1199  * @type String
1200  */
1201 LatLonPoint.prototype.toString = function() {
1202         return this.lat + ', ' + this.lon;
1203 };
1204
1205 /**
1206  * distance returns the distance in kilometers between two points
1207  * @param {LatLonPoint} otherPoint The other point to measure the distance from to this one
1208  * @returns the distance between the points in kilometers
1209  * @type double
1210  */
1211 LatLonPoint.prototype.distance = function(otherPoint) {
1212         // Uses Haversine formula from http://www.movable-type.co.uk
1213         var rads = Math.PI / 180;
1214         var diffLat = (this.lat-otherPoint.lat) * rads;
1215         var diffLon = (this.lon-otherPoint.lon) * rads; 
1216         var a = Math.sin(diffLat / 2) * Math.sin(diffLat / 2) +
1217                 Math.cos(this.lat*rads) * Math.cos(otherPoint.lat*rads) * 
1218                 Math.sin(diffLon/2) * Math.sin(diffLon/2); 
1219         return 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)) * 6371; // Earth's mean radius in km
1220 };
1221
1222 /**
1223  * equals tests if this point is the same as some other one
1224  * @param {LatLonPoint} otherPoint The other point to test with
1225  * @returns true or false
1226  * @type boolean
1227  */
1228 LatLonPoint.prototype.equals = function(otherPoint) {
1229         return this.lat == otherPoint.lat && this.lon == otherPoint.lon;
1230 };
1231
1232 /**
1233  * Returns latitude conversion based on current projection
1234  * @returns {Float} conversion
1235  */
1236 LatLonPoint.prototype.latConv = function() {
1237         return this.distance(new LatLonPoint(this.lat + 0.1, this.lon))*10;
1238 };
1239
1240 /**
1241  * Returns longitude conversion based on current projection
1242  * @returns {Float} conversion
1243  */
1244 LatLonPoint.prototype.lonConv = function() {
1245         return this.distance(new LatLonPoint(this.lat, this.lon + 0.1))*10;
1246 };
1247
1248
1249 //////////////////////////
1250 //
1251 //  BoundingBox
1252 //
1253 //////////////////////////
1254
1255 /**
1256  * BoundingBox creates a new bounding box object
1257  * @name mxn.BoundingBox
1258  * @constructor
1259  * @param {double} swlat the latitude of the south-west point
1260  * @param {double} swlon the longitude of the south-west point
1261  * @param {double} nelat the latitude of the north-east point
1262  * @param {double} nelon the longitude of the north-east point
1263  * @exports BoundingBox as mxn.BoundingBox
1264  */
1265 var BoundingBox = mxn.BoundingBox = function(swlat, swlon, nelat, nelon) {
1266         //FIXME throw error if box bigger than world
1267         //alert('new bbox ' + swlat + ',' +  swlon + ',' +  nelat + ',' + nelon);
1268         this.sw = new LatLonPoint(swlat, swlon);
1269         this.ne = new LatLonPoint(nelat, nelon);
1270 };
1271
1272 /**
1273  * getSouthWest returns a LatLonPoint of the south-west point of the bounding box
1274  * @returns the south-west point of the bounding box
1275  * @type LatLonPoint
1276  */
1277 BoundingBox.prototype.getSouthWest = function() {
1278         return this.sw;
1279 };
1280
1281 /**
1282  * getNorthEast returns a LatLonPoint of the north-east point of the bounding box
1283  * @returns the north-east point of the bounding box
1284  * @type LatLonPoint
1285  */
1286 BoundingBox.prototype.getNorthEast = function() {
1287         return this.ne;
1288 };
1289
1290 /**
1291  * isEmpty finds if this bounding box has zero area
1292  * @returns whether the north-east and south-west points of the bounding box are the same point
1293  * @type boolean
1294  */
1295 BoundingBox.prototype.isEmpty = function() {
1296         return this.ne == this.sw; // is this right? FIXME
1297 };
1298
1299 /**
1300  * contains finds whether a given point is within a bounding box
1301  * @param {LatLonPoint} point the point to test with
1302  * @returns whether point is within this bounding box
1303  * @type boolean
1304  */
1305 BoundingBox.prototype.contains = function(point){
1306         return point.lat >= this.sw.lat && point.lat <= this.ne.lat && point.lon >= this.sw.lon && point.lon <= this.ne.lon;
1307 };
1308
1309 /**
1310  * toSpan returns a LatLonPoint with the lat and lon as the height and width of the bounding box
1311  * @returns a LatLonPoint containing the height and width of this bounding box
1312  * @type LatLonPoint
1313  */
1314 BoundingBox.prototype.toSpan = function() {
1315         return new LatLonPoint( Math.abs(this.sw.lat - this.ne.lat), Math.abs(this.sw.lon - this.ne.lon) );
1316 };
1317
1318 /**
1319  * extend extends the bounding box to include the new point
1320  */
1321 BoundingBox.prototype.extend = function(point) {
1322         if(this.sw.lat > point.lat) {
1323                 this.sw.lat = point.lat;
1324         }
1325         if(this.sw.lon > point.lon) {
1326                 this.sw.lon = point.lon;
1327         }
1328         if(this.ne.lat < point.lat) {
1329                 this.ne.lat = point.lat;
1330         }
1331         if(this.ne.lon < point.lon) {
1332                 this.ne.lon = point.lon;
1333         }
1334         return;
1335 };
1336
1337 //////////////////////////////
1338 //
1339 //  Marker
1340 //
1341 ///////////////////////////////
1342
1343 /**
1344  * Marker create's a new marker pin
1345  * @name mxn.Marker
1346  * @constructor
1347  * @param {LatLonPoint} point the point on the map where the marker should go
1348  * @exports Marker as mxn.Marker
1349  */
1350 var Marker = mxn.Marker = function(point) {
1351         this.api = null;
1352         this.location = point;
1353         this.onmap = false;
1354         this.proprietary_marker = false;
1355         this.attributes = [];
1356         this.invoker = new mxn.Invoker(this, 'Marker', function(){return this.api;});
1357         mxn.addEvents(this, [ 
1358                 'openInfoBubble',       // Info bubble opened
1359                 'closeInfoBubble',      // Info bubble closed
1360                 'click'                         // Marker clicked
1361         ]);
1362 };
1363
1364 mxn.addProxyMethods(Marker, [ 
1365         'fromProprietary',
1366         'hide',
1367         'openBubble',
1368         'show',
1369         'toProprietary',
1370         'update'
1371 ]);
1372
1373 Marker.prototype.setChild = function(some_proprietary_marker) {
1374         this.proprietary_marker = some_proprietary_marker;
1375         some_proprietary_marker.mapstraction_marker = this;
1376         this.onmap = true;
1377 };
1378
1379 Marker.prototype.setLabel = function(labelText) {
1380         this.labelText = labelText;
1381 };
1382
1383 /**
1384  * addData conviniently set a hash of options on a marker
1385  */
1386 Marker.prototype.addData = function(options){
1387         for(var sOptKey in options) {
1388                 if(options.hasOwnProperty(sOptKey)){
1389                         switch(sOptKey) {
1390                                 case 'label':
1391                                         this.setLabel(options.label);
1392                                         break;
1393                                 case 'infoBubble':
1394                                         this.setInfoBubble(options.infoBubble);
1395                                         break;
1396                                 case 'icon':
1397                                         if(options.iconSize && options.iconAnchor) {
1398                                                 this.setIcon(options.icon, options.iconSize, options.iconAnchor);
1399                                         }
1400                                         else if(options.iconSize) {
1401                                                 this.setIcon(options.icon, options.iconSize);
1402                                         }
1403                                         else {
1404                                                 this.setIcon(options.icon);
1405                                         }
1406                                         break;
1407                                 case 'iconShadow':
1408                                         if(options.iconShadowSize) {
1409                                                 this.setShadowIcon(options.iconShadow, [ options.iconShadowSize[0], options.iconShadowSize[1] ]);
1410                                         }
1411                                         else {
1412                                                 this.setIcon(options.iconShadow);
1413                                         }
1414                                         break;
1415                                 case 'infoDiv':
1416                                         this.setInfoDiv(options.infoDiv[0],options.infoDiv[1]);
1417                                         break;
1418                                 case 'draggable':
1419                                         this.setDraggable(options.draggable);
1420                                         break;
1421                                 case 'hover':
1422                                         this.setHover(options.hover);
1423                                         this.setHoverIcon(options.hoverIcon);
1424                                         break;
1425                                 case 'hoverIcon':
1426                                         this.setHoverIcon(options.hoverIcon);
1427                                         break;
1428                                 case 'openBubble':
1429                                         this.openBubble();
1430                                         break;
1431                                 case 'groupName':
1432                                         this.setGroupName(options.groupName);
1433                                         break;
1434                                 default:
1435                                         // don't have a specific action for this bit of
1436                                         // data so set a named attribute
1437                                         this.setAttribute(sOptKey, options[sOptKey]);
1438                                         break;
1439                         }
1440                 }
1441         }
1442 };
1443
1444 /**
1445  * setInfoBubble sets the html/text content for a bubble popup for a marker
1446  * @param {String} infoBubble the html/text you want displayed
1447  */
1448 Marker.prototype.setInfoBubble = function(infoBubble) {
1449         this.infoBubble = infoBubble;
1450 };
1451
1452 /**
1453  * setInfoDiv sets the text and the id of the div element where to the information
1454  *  useful for putting information in a div outside of the map
1455  * @param {String} infoDiv the html/text you want displayed
1456  * @param {String} div the element id to use for displaying the text/html
1457  */
1458 Marker.prototype.setInfoDiv = function(infoDiv,div){
1459         this.infoDiv = infoDiv;
1460         this.div = div;
1461 };
1462
1463 /**
1464  * setIcon sets the icon for a marker
1465  * @param {String} iconUrl The URL of the image you want to be the icon
1466  */
1467 Marker.prototype.setIcon = function(iconUrl, iconSize, iconAnchor) {
1468         this.iconUrl = iconUrl;
1469         if(iconSize) {
1470                 this.iconSize = iconSize;
1471         }
1472         if(iconAnchor) {
1473                 this.iconAnchor = iconAnchor;
1474         }
1475 };
1476
1477 /**
1478  * setIconSize sets the size of the icon for a marker
1479  * @param {String} iconSize The array size in pixels of the marker image
1480  */
1481 Marker.prototype.setIconSize = function(iconSize){
1482         if(iconSize) {
1483                 this.iconSize = iconSize;
1484         }
1485 };
1486
1487 /**
1488  * setIconAnchor sets the anchor point for a marker
1489  * @param {String} iconAnchor The array offset of the anchor point
1490  */
1491 Marker.prototype.setIconAnchor = function(iconAnchor){
1492         if(iconAnchor) {
1493                 this.iconAnchor = iconAnchor;
1494         }
1495 };
1496
1497 /**
1498  * setShadowIcon sets the icon for a marker
1499  * @param {String} iconUrl The URL of the image you want to be the icon
1500  */
1501 Marker.prototype.setShadowIcon = function(iconShadowUrl, iconShadowSize){
1502         this.iconShadowUrl = iconShadowUrl;
1503         if(iconShadowSize) {
1504                 this.iconShadowSize = iconShadowSize;
1505         }
1506 };
1507
1508 Marker.prototype.setHoverIcon = function(hoverIconUrl){
1509         this.hoverIconUrl = hoverIconUrl;
1510 };
1511
1512 /**
1513  * setDraggable sets the draggable state of the marker
1514  * @param {Bool} draggable set to true if marker should be draggable by the user
1515  */
1516 Marker.prototype.setDraggable = function(draggable) {
1517         this.draggable = draggable;
1518 };
1519
1520 /**
1521  * setHover sets that the marker info is displayed on hover
1522  * @param {Bool} hover set to true if marker should display info on hover
1523  */
1524 Marker.prototype.setHover = function(hover) {
1525         this.hover = hover;
1526 };
1527
1528 /**
1529  * Markers are grouped up by this name. declutterGroup makes use of this.
1530  */
1531 Marker.prototype.setGroupName = function(sGrpName) {
1532         this.groupName = sGrpName;
1533 };
1534
1535 /**
1536  * setAttribute: set an arbitrary key/value pair on a marker
1537  * @arg(String) key
1538  * @arg value
1539  */
1540 Marker.prototype.setAttribute = function(key,value) {
1541         this.attributes[key] = value;
1542 };
1543
1544 /**
1545  * getAttribute: gets the value of "key"
1546  * @arg(String) key
1547  * @returns value
1548  */
1549 Marker.prototype.getAttribute = function(key) {
1550         return this.attributes[key];
1551 };
1552
1553
1554 ///////////////
1555 // Polyline ///
1556 ///////////////
1557
1558 /**
1559  * Instantiates a new Polyline.
1560  * @name mxn.Polyline
1561  * @constructor
1562  * @param {Point[]} points Points that make up the Polyline.
1563  * @exports Polyline as mxn.Polyline
1564  */
1565 var Polyline = mxn.Polyline = function(points) {
1566         this.api = null;
1567         this.points = points;
1568         this.attributes = [];
1569         this.onmap = false;
1570         this.proprietary_polyline = false;
1571         this.pllID = "mspll-"+new Date().getTime()+'-'+(Math.floor(Math.random()*Math.pow(2,16)));
1572         this.invoker = new mxn.Invoker(this, 'Polyline', function(){return this.api;});
1573 };
1574
1575 mxn.addProxyMethods(Polyline, [ 
1576         'fromProprietary', 
1577         'hide',
1578         'show',
1579         'toProprietary',
1580         'update'
1581 ]);
1582
1583 /**
1584  * addData conviniently set a hash of options on a polyline
1585  */
1586 Polyline.prototype.addData = function(options){
1587         for(var sOpt in options) {
1588                 if(options.hasOwnProperty(sOpt)){
1589                         switch(sOpt) {
1590                                 case 'color':
1591                                         this.setColor(options.color);
1592                                         break;
1593                                 case 'width':
1594                                         this.setWidth(options.width);
1595                                         break;
1596                                 case 'opacity':
1597                                         this.setOpacity(options.opacity);
1598                                         break;
1599                                 case 'closed':
1600                                         this.setClosed(options.closed);
1601                                         break;
1602                                 case 'fillColor':
1603                                         this.setFillColor(options.fillColor);
1604                                         break;
1605                                 default:
1606                                         this.setAttribute(sOpt, options[sOpt]);
1607                                         break;
1608                         }
1609                 }
1610         }
1611 };
1612
1613 Polyline.prototype.setChild = function(some_proprietary_polyline) {
1614         this.proprietary_polyline = some_proprietary_polyline;
1615         this.onmap = true;
1616 };
1617
1618 /**
1619  * in the form: #RRGGBB
1620  * Note map24 insists on upper case, so we convert it.
1621  */
1622 Polyline.prototype.setColor = function(color){
1623         this.color = (color.length==7 && color[0]=="#") ? color.toUpperCase() : color;
1624 };
1625
1626 /**
1627  * Stroke width of the polyline
1628  * @param {Integer} width
1629  */
1630 Polyline.prototype.setWidth = function(width){
1631         this.width = width;
1632 };
1633
1634 /**
1635  * A float between 0.0 and 1.0
1636  * @param {Float} opacity
1637  */
1638 Polyline.prototype.setOpacity = function(opacity){
1639         this.opacity = opacity;
1640 };
1641
1642 /**
1643  * Marks the polyline as a closed polygon
1644  * @param {Boolean} bClosed
1645  */
1646 Polyline.prototype.setClosed = function(bClosed){
1647         this.closed = bClosed;
1648 };
1649
1650 /**
1651  * Fill color for a closed polyline as HTML color value e.g. #RRGGBB
1652  * @param {String} sFillColor HTML color value #RRGGBB
1653  */
1654 Polyline.prototype.setFillColor = function(sFillColor) {
1655         this.fillColor = sFillColor;
1656 };
1657
1658
1659 /**
1660  * setAttribute: set an arbitrary key/value pair on a polyline
1661  * @arg(String) key
1662  * @arg value
1663  */
1664 Polyline.prototype.setAttribute = function(key,value) {
1665         this.attributes[key] = value;
1666 };
1667
1668 /**
1669  * getAttribute: gets the value of "key"
1670  * @arg(String) key
1671  * @returns value
1672  */
1673 Polyline.prototype.getAttribute = function(key) {
1674         return this.attributes[key];
1675 };
1676
1677 /**
1678  * Simplifies a polyline, averaging and reducing the points
1679  * @param {Integer} tolerance (1.0 is a good starting point)
1680  */
1681 Polyline.prototype.simplify = function(tolerance) {
1682         var reduced = [];
1683
1684         // First point
1685         reduced[0] = this.points[0];
1686
1687         var markerPoint = 0;
1688
1689         for (var i = 1; i < this.points.length-1; i++){
1690                 if (this.points[i].distance(this.points[markerPoint]) >= tolerance)
1691                 {
1692                         reduced[reduced.length] = this.points[i];
1693                         markerPoint = i;
1694                 }
1695         }
1696
1697         // Last point
1698         reduced[reduced.length] = this.points[this.points.length-1];
1699
1700         // Revert
1701         this.points = reduced;
1702 };
1703
1704 ///////////////
1705 // Radius    //
1706 ///////////////
1707
1708 /**
1709  * Creates a new radius object for drawing circles around a point, does a lot of initial calculation to increase load time
1710  * @returns a new Radius
1711  * @type Radius
1712  * @constructor
1713  * @classDescription Radius
1714  * @param {Object} Center LatLonPoint of the radius
1715  * @param {quality} Number of points that comprise the approximated circle (20 is a good starting point)
1716  */
1717 var Radius = mxn.Radius = function(center, quality) {
1718         this.center = center;
1719         var latConv = center.latConv();
1720         var lonConv = center.lonConv();
1721
1722         // Create Radian conversion constant
1723         var rad = Math.PI / 180;
1724         this.calcs = [];
1725
1726         for(var i = 0; i < 360; i += quality){
1727             this.calcs.push([Math.cos(i * rad) / latConv, Math.sin(i * rad) / lonConv]);
1728         }
1729 };
1730
1731 /**
1732  * Returns polyline of a circle around the point based on new radius
1733  * @param {Radius} radius
1734  * @param {Colour} colour
1735  * @returns {Polyline} Polyline
1736  */
1737 Radius.prototype.getPolyline = function(radius, colour) {
1738         var points = [];
1739
1740         for(var i = 0; i < this.calcs.length; i++){
1741                 var point = new LatLonPoint(
1742                         this.center.lat + (radius * this.calcs[i][0]),
1743                         this.center.lon + (radius * this.calcs[i][1])
1744                 );
1745                 points.push(point);
1746         }
1747
1748         // Add first point
1749         points.push(points[0]);
1750
1751         var line = new Polyline(points);
1752         line.setColor(colour);
1753
1754         return line;
1755 };
1756
1757
1758 })();