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