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