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 					iconAnchor : item.icon_anchor,
948 					category : item.source_id,
949 					draggable : false,
950 					hover : false
951 				});
952 				break;
953 			case "Polygon":
954 				var points = [];
955 				polyline = new Polyline(points);
956 				mapstraction.addPolylineWithData(polyline,{
957 					fillColor : item.poly_color,
958 					date : "new Date(\""+item.date+"\")",
959 					category : item.source_id,
960 					width : item.line_width,
961 					opacity : item.line_opacity,
962 					color : item.line_color,
963 					polygon : true
964 				});
965 				markers.push(polyline);
966 				break;
967 			default:
968 		// console.log("Geometry: " + features.items[i].geometry.type);
969 		}
970 	}
971 	return markers;
972 };
973 
974 /**
975  * Adds a Tile Layer to the map
976  *
977  * Requires providing a parameterized tile url. Use {Z}, {X}, and {Y} to specify where the parameters
978  *  should go in the URL.
979  *
980  * For example, the OpenStreetMap tiles are:
981  *  m.addTileLayer("http://tile.openstreetmap.org/{Z}/{X}/{Y}.png", 1.0, "OSM", 1, 19, true);
982  *
983  * @param {tile_url} template url of the tiles.
984  * @param {opacity} opacity of the tile layer - 0 is transparent, 1 is opaque. (default=0.6)
985  * @param {copyright_text} copyright text to use for the tile layer. (default=Mapstraction)
986  * @param {min_zoom} Minimum (furtherest out) zoom level that tiles are available (default=1)
987  * @param {max_zoom} Maximum (closest) zoom level that the tiles are available (default=18)
988  * @param {map_type} Should the tile layer be a selectable map type in the layers palette (default=false)
989  */
990 Mapstraction.prototype.addTileLayer = function(tile_url, opacity, copyright_text, min_zoom, max_zoom, map_type) {
991 	if(!tile_url) {
992 		return;
993 	}
994 	
995 	opacity = opacity || 0.6;
996 	copyright_text = copyright_text || "Mapstraction";
997 	min_zoom = min_zoom || 1;
998 	max_zoom = max_zoom || 18;
999 	map_type = map_type || false;
1000 
1001 	return this.invoker.go('addTileLayer', [ tile_url, opacity, copyright_text, min_zoom, max_zoom, map_type] );
1002 };
1003 
1004 /**
1005  * addFilter adds a marker filter
1006  * @param {field} name of attribute to filter on
1007  * @param {operator} presently only "ge" or "le"
1008  * @param {value} the value to compare against
1009  */
1010 Mapstraction.prototype.addFilter = function(field, operator, value) {
1011 	if (!this.filters) {
1012 		this.filters = [];
1013 	}
1014 	this.filters.push( [field, operator, value] );
1015 };
1016 
1017 /**
1018  * Remove the specified filter
1019  * @param {Object} field
1020  * @param {Object} operator
1021  * @param {Object} value
1022  */
1023 Mapstraction.prototype.removeFilter = function(field, operator, value) {
1024 	if (!this.filters) {
1025 		return;
1026 	}
1027 
1028 	var del;
1029 	for (var f=0; f<this.filters.length; f++) {
1030 		if (this.filters[f][0] == field &&
1031 			(! operator || (this.filters[f][1] == operator && this.filters[f][2] == value))) {
1032 			this.filters.splice(f,1);
1033 			f--; //array size decreased
1034 		}
1035 	}
1036 };
1037 
1038 /**
1039  * Delete the current filter if present; otherwise add it
1040  * @param {Object} field
1041  * @param {Object} operator
1042  * @param {Object} value
1043  */
1044 Mapstraction.prototype.toggleFilter = function(field, operator, value) {
1045 	if (!this.filters) {
1046 		this.filters = [];
1047 	}
1048 
1049 	var found = false;
1050 	for (var f = 0; f < this.filters.length; f++) {
1051 		if (this.filters[f][0] == field && this.filters[f][1] == operator && this.filters[f][2] == value) {
1052 			this.filters.splice(f,1);
1053 			f--; //array size decreased
1054 			found = true;
1055 		}
1056 	}
1057 
1058 	if (! found) {
1059 		this.addFilter(field, operator, value);
1060 	}
1061 };
1062 
1063 /**
1064  * removeAllFilters
1065  */
1066 Mapstraction.prototype.removeAllFilters = function() {
1067 	this.filters = [];
1068 };
1069 
1070 /**
1071  * doFilter executes all filters added since last call
1072  * Now supports a callback function for when a marker is shown or hidden
1073  * @param {Function} showCallback
1074  * @param {Function} hideCallback
1075  * @returns {Int} count of visible markers
1076  */
1077 Mapstraction.prototype.doFilter = function(showCallback, hideCallback) {
1078 	var map = this.maps[this.api];
1079 	var visibleCount = 0;
1080 	var f;
1081 	if (this.filters) {
1082 		switch (this.api) {
1083 			case 'multimap':
1084 				/* TODO polylines aren't filtered in multimap */
1085 				var mmfilters = [];
1086 				for (f=0; f<this.filters.length; f++) {
1087 					mmfilters.push( new MMSearchFilter( this.filters[f][0], this.filters[f][1], this.filters[f][2] ));
1088 				}
1089 				map.setMarkerFilters( mmfilters );
1090 				map.redrawMap();
1091 				break;
1092 			case '  dummy':
1093 				break;
1094 			default:
1095 				var vis;
1096 				for (var m=0; m<this.markers.length; m++) {
1097 					vis = true;
1098 					for (f = 0; f < this.filters.length; f++) {
1099 						if (! this.applyFilter(this.markers[m], this.filters[f])) {
1100 							vis = false;
1101 						}
1102 					}
1103 					if (vis) {
1104 						visibleCount ++;
1105 						if (showCallback){
1106 							showCallback(this.markers[m]);
1107 						}
1108 						else {
1109 							this.markers[m].show();
1110 						}
1111 					} 
1112 					else { 
1113 						if (hideCallback){
1114 							hideCallback(this.markers[m]);
1115 						}
1116 						else {
1117 							this.markers[m].hide();
1118 						}
1119 					}
1120 
1121 					this.markers[m].setAttribute("visible", vis);
1122 				}
1123 				break;
1124 		}
1125 	}
1126 	return visibleCount;
1127 };
1128 
1129 Mapstraction.prototype.applyFilter = function(o, f) {
1130 	var vis = true;
1131 	switch (f[1]) {
1132 		case 'ge':
1133 			if (o.getAttribute( f[0] ) < f[2]) {
1134 				vis = false;
1135 			}
1136 			break;
1137 		case 'le':
1138 			if (o.getAttribute( f[0] ) > f[2]) {
1139 				vis = false;
1140 			}
1141 			break;
1142 		case 'eq':
1143 			if (o.getAttribute( f[0] ) == f[2]) {
1144 				vis = false;
1145 			}
1146 			break;
1147 	}
1148 
1149 	return vis;
1150 };
1151 
1152 /**
1153  * getAttributeExtremes returns the minimum/maximum of "field" from all markers
1154  * @param {field} name of "field" to query
1155  * @returns {array} of minimum/maximum
1156  */
1157 Mapstraction.prototype.getAttributeExtremes = function(field) {
1158 	var min;
1159 	var max;
1160 	for (var m=0; m<this.markers.length; m++) {
1161 		if (! min || min > this.markers[m].getAttribute(field)) {
1162 			min = this.markers[m].getAttribute(field);
1163 		}
1164 		if (! max || max < this.markers[m].getAttribute(field)) {
1165 			max = this.markers[m].getAttribute(field);
1166 		}
1167 	}
1168 	for (var p=0; m<this.polylines.length; m++) {
1169 		if (! min || min > this.polylines[p].getAttribute(field)) {
1170 			min = this.polylines[p].getAttribute(field);
1171 		}
1172 		if (! max || max < this.polylines[p].getAttribute(field)) {
1173 			max = this.polylines[p].getAttribute(field);
1174 		}
1175 	}
1176 
1177 	return [min, max];
1178 };
1179 
1180 /**
1181  * getMap returns the native map object that mapstraction is talking to
1182  * @returns the native map object mapstraction is using
1183  */
1184 Mapstraction.prototype.getMap = function() {
1185 	// FIXME in an ideal world this shouldn't exist right?
1186 	return this.maps[this.api];
1187 };
1188 
1189 
1190 //////////////////////////////
1191 //
1192 //   LatLonPoint
1193 //
1194 /////////////////////////////
1195 
1196 /**
1197  * LatLonPoint is a point containing a latitude and longitude with helper methods
1198  * @name mxn.LatLonPoint
1199  * @constructor
1200  * @param {double} lat is the latitude
1201  * @param {double} lon is the longitude
1202  * @exports LatLonPoint as mxn.LatLonPoint
1203  */
1204 var LatLonPoint = mxn.LatLonPoint = function(lat, lon) {	
1205 	this.lat = Number(lat); // force to be numeric
1206 	this.lon = Number(lon);
1207 	this.lng = this.lon; // lets be lon/lng agnostic
1208 	
1209 	this.invoker = new mxn.Invoker(this, 'LatLonPoint');		
1210 };
1211 
1212 mxn.addProxyMethods(LatLonPoint, [ 
1213 	/**
1214 	 * Retrieve the lat and lon values from a proprietary point.
1215 	 * @name mxn.LatLonPoint#fromProprietary
1216 	 * @function
1217 	 * @param {String} apiId The API ID of the proprietary point.
1218 	 * @param {Object} point The proprietary point.
1219 	 */
1220 	'fromProprietary',
1221 	
1222 	/**
1223 	 * Converts the current LatLonPoint to a proprietary one for the API specified by apiId.
1224 	 * @name mxn.LatLonPoint#toProprietary
1225 	 * @function
1226 	 * @param {String} apiId The API ID of the proprietary point.
1227 	 * @returns A proprietary point.
1228 	 */
1229 	'toProprietary'
1230 ], true);
1231 
1232 /**
1233  * toString returns a string represntation of a point
1234  * @returns a string like '51.23, -0.123'
1235  * @type String
1236  */
1237 LatLonPoint.prototype.toString = function() {
1238 	return this.lat + ', ' + this.lon;
1239 };
1240 
1241 /**
1242  * distance returns the distance in kilometers between two points
1243  * @param {LatLonPoint} otherPoint The other point to measure the distance from to this one
1244  * @returns the distance between the points in kilometers
1245  * @type double
1246  */
1247 LatLonPoint.prototype.distance = function(otherPoint) {
1248 	// Uses Haversine formula from http://www.movable-type.co.uk
1249 	var rads = Math.PI / 180;
1250 	var diffLat = (this.lat-otherPoint.lat) * rads;
1251 	var diffLon = (this.lon-otherPoint.lon) * rads; 
1252 	var a = Math.sin(diffLat / 2) * Math.sin(diffLat / 2) +
1253 		Math.cos(this.lat*rads) * Math.cos(otherPoint.lat*rads) * 
1254 		Math.sin(diffLon/2) * Math.sin(diffLon/2); 
1255 	return 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)) * 6371; // Earth's mean radius in km
1256 };
1257 
1258 /**
1259  * equals tests if this point is the same as some other one
1260  * @param {LatLonPoint} otherPoint The other point to test with
1261  * @returns true or false
1262  * @type boolean
1263  */
1264 LatLonPoint.prototype.equals = function(otherPoint) {
1265 	return this.lat == otherPoint.lat && this.lon == otherPoint.lon;
1266 };
1267 
1268 /**
1269  * Returns latitude conversion based on current projection
1270  * @returns {Float} conversion
1271  */
1272 LatLonPoint.prototype.latConv = function() {
1273 	return this.distance(new LatLonPoint(this.lat + 0.1, this.lon))*10;
1274 };
1275 
1276 /**
1277  * Returns longitude conversion based on current projection
1278  * @returns {Float} conversion
1279  */
1280 LatLonPoint.prototype.lonConv = function() {
1281 	return this.distance(new LatLonPoint(this.lat, this.lon + 0.1))*10;
1282 };
1283 
1284 
1285 //////////////////////////
1286 //
1287 //  BoundingBox
1288 //
1289 //////////////////////////
1290 
1291 /**
1292  * BoundingBox creates a new bounding box object
1293  * @name mxn.BoundingBox
1294  * @constructor
1295  * @param {double} swlat the latitude of the south-west point
1296  * @param {double} swlon the longitude of the south-west point
1297  * @param {double} nelat the latitude of the north-east point
1298  * @param {double} nelon the longitude of the north-east point
1299  * @exports BoundingBox as mxn.BoundingBox
1300  */
1301 var BoundingBox = mxn.BoundingBox = function(swlat, swlon, nelat, nelon) {
1302 	//FIXME throw error if box bigger than world
1303 	this.sw = new LatLonPoint(swlat, swlon);
1304 	this.ne = new LatLonPoint(nelat, nelon);
1305 };
1306 
1307 /**
1308  * getSouthWest returns a LatLonPoint of the south-west point of the bounding box
1309  * @returns the south-west point of the bounding box
1310  * @type LatLonPoint
1311  */
1312 BoundingBox.prototype.getSouthWest = function() {
1313 	return this.sw;
1314 };
1315 
1316 /**
1317  * getNorthEast returns a LatLonPoint of the north-east point of the bounding box
1318  * @returns the north-east point of the bounding box
1319  * @type LatLonPoint
1320  */
1321 BoundingBox.prototype.getNorthEast = function() {
1322 	return this.ne;
1323 };
1324 
1325 /**
1326  * isEmpty finds if this bounding box has zero area
1327  * @returns whether the north-east and south-west points of the bounding box are the same point
1328  * @type boolean
1329  */
1330 BoundingBox.prototype.isEmpty = function() {
1331 	return this.ne == this.sw; // is this right? FIXME
1332 };
1333 
1334 /**
1335  * contains finds whether a given point is within a bounding box
1336  * @param {LatLonPoint} point the point to test with
1337  * @returns whether point is within this bounding box
1338  * @type boolean
1339  */
1340 BoundingBox.prototype.contains = function(point){
1341 	return point.lat >= this.sw.lat && point.lat <= this.ne.lat && point.lon >= this.sw.lon && point.lon <= this.ne.lon;
1342 };
1343 
1344 /**
1345  * toSpan returns a LatLonPoint with the lat and lon as the height and width of the bounding box
1346  * @returns a LatLonPoint containing the height and width of this bounding box
1347  * @type LatLonPoint
1348  */
1349 BoundingBox.prototype.toSpan = function() {
1350 	return new LatLonPoint( Math.abs(this.sw.lat - this.ne.lat), Math.abs(this.sw.lon - this.ne.lon) );
1351 };
1352 
1353 
1354 
1355 /**
1356  * extend extends the bounding box to include the new point
1357  */
1358 BoundingBox.prototype.extend = function(point) {
1359 	if (this.sw.lat > point.lat) {
1360 		this.sw.lat = point.lat;
1361 	}
1362 	if (this.sw.lon > point.lon) {
1363 		this.sw.lon = point.lon;
1364 	}
1365 	if (this.ne.lat < point.lat) {
1366 		this.ne.lat = point.lat;
1367 	}
1368 	if (this.ne.lon < point.lon) {
1369 		this.ne.lon = point.lon;
1370 	}
1371 	return;
1372 };
1373 
1374 //////////////////////////////
1375 //
1376 //  Marker
1377 //
1378 ///////////////////////////////
1379 
1380 /**
1381  * Marker create's a new marker pin
1382  * @name mxn.Marker
1383  * @constructor
1384  * @param {LatLonPoint} point the point on the map where the marker should go
1385  * @exports Marker as mxn.Marker
1386  */
1387 var Marker = mxn.Marker = function(point) {
1388 	this.api = null;
1389 	this.location = point;
1390 	this.onmap = false;
1391 	this.proprietary_marker = false;
1392 	this.attributes = [];
1393 	this.invoker = new mxn.Invoker(this, 'Marker', function(){return this.api;});
1394 	mxn.addEvents(this, [ 
1395 		'openInfoBubble',	// Info bubble opened
1396 		'closeInfoBubble', 	// Info bubble closed
1397 		'click'				// Marker clicked
1398 	]);
1399 };
1400 
1401 mxn.addProxyMethods(Marker, [ 
1402 	/**
1403 	 * Retrieve the settings from a proprietary marker.
1404 	 * @name mxn.Marker#fromProprietary
1405 	 * @function
1406 	 * @param {String} apiId The API ID of the proprietary point.
1407 	 * @param {Object} marker The proprietary marker.
1408 	 */
1409 	'fromProprietary',
1410 	
1411 	/**
1412 	 * Hide the marker.
1413 	 * @name mxn.Marker#hide
1414 	 * @function
1415 	 */
1416 	'hide',
1417 	
1418 	/**
1419 	 * Open the marker's info bubble.
1420 	 * @name mxn.Marker#openBubble
1421 	 * @function
1422 	 */
1423 	'openBubble',
1424 	
1425 	/**
1426 	 * Closes the marker's info bubble.
1427 	 * @name mxn.Marker#closeBubble
1428 	 * @function
1429 	 */
1430 	'closeBubble',
1431 	
1432 	/**
1433 	 * Show the marker.
1434 	 * @name mxn.Marker#show
1435 	 * @function
1436 	 */
1437 	'show',
1438 	
1439 	/**
1440 	 * Converts the current Marker to a proprietary one for the API specified by apiId.
1441 	 * @name mxn.Marker#toProprietary
1442 	 * @function
1443 	 * @param {String} apiId The API ID of the proprietary marker.
1444 	 * @returns A proprietary marker.
1445 	 */
1446 	'toProprietary',
1447 	
1448 	/**
1449 	 * Updates the Marker with the location of the attached proprietary marker on the map.
1450 	 * @name mxn.Marker#update
1451 	 * @function
1452 	 */
1453 	'update'
1454 ]);
1455 
1456 Marker.prototype.setChild = function(some_proprietary_marker) {
1457 	this.proprietary_marker = some_proprietary_marker;
1458 	some_proprietary_marker.mapstraction_marker = this;
1459 	this.onmap = true;
1460 };
1461 
1462 Marker.prototype.setLabel = function(labelText) {
1463 	this.labelText = labelText;
1464 };
1465 
1466 /**
1467  * addData conviniently set a hash of options on a marker
1468  * @param {Object} options An object literal hash of key value pairs. Keys are: label, infoBubble, icon, iconShadow, infoDiv, draggable, hover, hoverIcon, openBubble, groupName.
1469  */
1470 Marker.prototype.addData = function(options){
1471 	for(var sOptKey in options) {
1472 		if(options.hasOwnProperty(sOptKey)){
1473 			switch(sOptKey) {
1474 				case 'label':
1475 					this.setLabel(options.label);
1476 					break;
1477 				case 'infoBubble':
1478 					this.setInfoBubble(options.infoBubble);
1479 					break;
1480 				case 'icon':
1481 					if(options.iconSize && options.iconAnchor) {
1482 						this.setIcon(options.icon, options.iconSize, options.iconAnchor);
1483 					}
1484 					else if(options.iconSize) {
1485 						this.setIcon(options.icon, options.iconSize);
1486 					}
1487 					else {
1488 						this.setIcon(options.icon);
1489 					}
1490 					break;
1491 				case 'iconShadow':
1492 					if(options.iconShadowSize) {
1493 						this.setShadowIcon(options.iconShadow, [ options.iconShadowSize[0], options.iconShadowSize[1] ]);
1494 					}
1495 					else {
1496 						this.setIcon(options.iconShadow);
1497 					}
1498 					break;
1499 				case 'infoDiv':
1500 					this.setInfoDiv(options.infoDiv[0],options.infoDiv[1]);
1501 					break;
1502 				case 'draggable':
1503 					this.setDraggable(options.draggable);
1504 					break;
1505 				case 'hover':
1506 					this.setHover(options.hover);
1507 					this.setHoverIcon(options.hoverIcon);
1508 					break;
1509 				case 'hoverIcon':
1510 					this.setHoverIcon(options.hoverIcon);
1511 					break;
1512 				case 'openBubble':
1513 					this.openBubble();
1514 					break;
1515 				case 'closeBubble':
1516 					this.closeBubble();
1517 					break;
1518 				case 'groupName':
1519 					this.setGroupName(options.groupName);
1520 					break;
1521 				default:
1522 					// don't have a specific action for this bit of
1523 					// data so set a named attribute
1524 					this.setAttribute(sOptKey, options[sOptKey]);
1525 					break;
1526 			}
1527 		}
1528 	}
1529 };
1530 
1531 /**
1532  * Sets the html/text content for a bubble popup for a marker
1533  * @param {String} infoBubble the html/text you want displayed
1534  */
1535 Marker.prototype.setInfoBubble = function(infoBubble) {
1536 	this.infoBubble = infoBubble;
1537 };
1538 
1539 /**
1540  * Sets the text and the id of the div element where to the information
1541  * useful for putting information in a div outside of the map
1542  * @param {String} infoDiv the html/text you want displayed
1543  * @param {String} div the element id to use for displaying the text/html
1544  */
1545 Marker.prototype.setInfoDiv = function(infoDiv,div){
1546 	this.infoDiv = infoDiv;
1547 	this.div = div;
1548 };
1549 
1550 /**
1551  * Sets the icon for a marker
1552  * @param {String} iconUrl The URL of the image you want to be the icon
1553  */
1554 Marker.prototype.setIcon = function(iconUrl, iconSize, iconAnchor) {
1555 	this.iconUrl = iconUrl;
1556 	if(iconSize) {
1557 		this.iconSize = iconSize;
1558 	}
1559 	if(iconAnchor) {
1560 		this.iconAnchor = iconAnchor;
1561 	}
1562 };
1563 
1564 /**
1565  * Sets the size of the icon for a marker
1566  * @param {Array} iconSize The array size in pixels of the marker image: [ width, height ]
1567  */
1568 Marker.prototype.setIconSize = function(iconSize){
1569 	if(iconSize) {
1570 		this.iconSize = iconSize;
1571 	}
1572 };
1573 
1574 /**
1575  * Sets the anchor point for a marker
1576  * @param {Array} iconAnchor The array offset in pixels of the anchor point from top left: [ right, down ]
1577  */
1578 Marker.prototype.setIconAnchor = function(iconAnchor){
1579 	if(iconAnchor) {
1580 		this.iconAnchor = iconAnchor;
1581 	}
1582 };
1583 
1584 /**
1585  * Sets the icon for a marker
1586  * @param {String} iconUrl The URL of the image you want to be the icon
1587  */
1588 Marker.prototype.setShadowIcon = function(iconShadowUrl, iconShadowSize){
1589 	this.iconShadowUrl = iconShadowUrl;
1590 	if(iconShadowSize) {
1591 		this.iconShadowSize = iconShadowSize;
1592 	}
1593 };
1594 
1595 Marker.prototype.setHoverIcon = function(hoverIconUrl){
1596 	this.hoverIconUrl = hoverIconUrl;
1597 };
1598 
1599 /**
1600  * Sets the draggable state of the marker
1601  * @param {Bool} draggable set to true if marker should be draggable by the user
1602  */
1603 Marker.prototype.setDraggable = function(draggable) {
1604 	this.draggable = draggable;
1605 };
1606 
1607 /**
1608  * Sets that the marker info is displayed on hover
1609  * @param {Boolean} hover set to true if marker should display info on hover
1610  */
1611 Marker.prototype.setHover = function(hover) {
1612 	this.hover = hover;
1613 };
1614 
1615 /**
1616  * Markers are grouped up by this name. declutterGroup makes use of this.
1617  */
1618 Marker.prototype.setGroupName = function(sGrpName) {
1619 	this.groupName = sGrpName;
1620 };
1621 
1622 /**
1623  * Set an arbitrary key/value pair on a marker
1624  * @param {String} key
1625  * @param value
1626  */
1627 Marker.prototype.setAttribute = function(key,value) {
1628 	this.attributes[key] = value;
1629 };
1630 
1631 /**
1632  * getAttribute: gets the value of "key"
1633  * @param {String} key
1634  * @returns value
1635  */
1636 Marker.prototype.getAttribute = function(key) {
1637 	return this.attributes[key];
1638 };
1639 
1640 
1641 ///////////////
1642 // Polyline ///
1643 ///////////////
1644 
1645 /**
1646  * Instantiates a new Polyline.
1647  * @name mxn.Polyline
1648  * @constructor
1649  * @param {Point[]} points Points that make up the Polyline.
1650  * @exports Polyline as mxn.Polyline
1651  */
1652 var Polyline = mxn.Polyline = function(points) {
1653 	this.api = null;
1654 	this.points = points;
1655 	this.attributes = [];
1656 	this.onmap = false;
1657 	this.proprietary_polyline = false;
1658 	this.pllID = "mspll-"+new Date().getTime()+'-'+(Math.floor(Math.random()*Math.pow(2,16)));
1659 	this.invoker = new mxn.Invoker(this, 'Polyline', function(){return this.api;});
1660 };
1661 
1662 mxn.addProxyMethods(Polyline, [ 
1663 
1664 	/**
1665 	 * Retrieve the settings from a proprietary polyline.
1666 	 * @name mxn.Polyline#fromProprietary
1667 	 * @function
1668 	 * @param {String} apiId The API ID of the proprietary polyline.
1669 	 * @param {Object} polyline The proprietary polyline.
1670 	 */
1671 	'fromProprietary', 
1672 	
1673 	/**
1674 	 * Hide the polyline.
1675 	 * @name mxn.Polyline#hide
1676 	 * @function
1677 	 */
1678 	'hide',
1679 	
1680 	/**
1681 	 * Show the polyline.
1682 	 * @name mxn.Polyline#show
1683 	 * @function
1684 	 */
1685 	'show',
1686 	
1687 	/**
1688 	 * Converts the current Polyline to a proprietary one for the API specified by apiId.
1689 	 * @name mxn.Polyline#toProprietary
1690 	 * @function
1691 	 * @param {String} apiId The API ID of the proprietary polyline.
1692 	 * @returns A proprietary polyline.
1693 	 */
1694 	'toProprietary',
1695 	
1696 	/**
1697 	 * Updates the Polyline with the path of the attached proprietary polyline on the map.
1698 	 * @name mxn.Polyline#update
1699 	 * @function
1700 	 */
1701 	'update'
1702 ]);
1703 
1704 /**
1705  * addData conviniently set a hash of options on a polyline
1706  * @param {Object} options An object literal hash of key value pairs. Keys are: color, width, opacity, closed, fillColor.
1707  */
1708 Polyline.prototype.addData = function(options){
1709 	for(var sOpt in options) {
1710 		if(options.hasOwnProperty(sOpt)){
1711 			switch(sOpt) {
1712 				case 'color':
1713 					this.setColor(options.color);
1714 					break;
1715 				case 'width':
1716 					this.setWidth(options.width);
1717 					break;
1718 				case 'opacity':
1719 					this.setOpacity(options.opacity);
1720 					break;
1721 				case 'closed':
1722 					this.setClosed(options.closed);
1723 					break;
1724 				case 'fillColor':
1725 					this.setFillColor(options.fillColor);
1726 					break;
1727 				default:
1728 					this.setAttribute(sOpt, options[sOpt]);
1729 					break;
1730 			}
1731 		}
1732 	}
1733 };
1734 
1735 Polyline.prototype.setChild = function(some_proprietary_polyline) {
1736 	this.proprietary_polyline = some_proprietary_polyline;
1737 	this.onmap = true;
1738 };
1739 
1740 /**
1741  * in the form: #RRGGBB
1742  * Note map24 insists on upper case, so we convert it.
1743  */
1744 Polyline.prototype.setColor = function(color){
1745 	this.color = (color.length==7 && color[0]=="#") ? color.toUpperCase() : color;
1746 };
1747 
1748 /**
1749  * Stroke width of the polyline
1750  * @param {Integer} width
1751  */
1752 Polyline.prototype.setWidth = function(width){
1753 	this.width = width;
1754 };
1755 
1756 /**
1757  * A float between 0.0 and 1.0
1758  * @param {Float} opacity
1759  */
1760 Polyline.prototype.setOpacity = function(opacity){
1761 	this.opacity = opacity;
1762 };
1763 
1764 /**
1765  * Marks the polyline as a closed polygon
1766  * @param {Boolean} bClosed
1767  */
1768 Polyline.prototype.setClosed = function(bClosed){
1769 	this.closed = bClosed;
1770 };
1771 
1772 /**
1773  * Fill color for a closed polyline as HTML color value e.g. #RRGGBB
1774  * @param {String} sFillColor HTML color value #RRGGBB
1775  */
1776 Polyline.prototype.setFillColor = function(sFillColor) {
1777 	this.fillColor = sFillColor;
1778 };
1779 
1780 
1781 /**
1782  * Set an arbitrary key/value pair on a polyline
1783  * @param {String} key
1784  * @param value
1785  */
1786 Polyline.prototype.setAttribute = function(key,value) {
1787 	this.attributes[key] = value;
1788 };
1789 
1790 /**
1791  * Gets the value of "key"
1792  * @param {String} key
1793  * @returns value
1794  */
1795 Polyline.prototype.getAttribute = function(key) {
1796 	return this.attributes[key];
1797 };
1798 
1799 /**
1800  * Simplifies a polyline, averaging and reducing the points
1801  * @param {Number} tolerance (1.0 is a good starting point)
1802  */
1803 Polyline.prototype.simplify = function(tolerance) {
1804 	var reduced = [];
1805 
1806 	// First point
1807 	reduced[0] = this.points[0];
1808 
1809 	var markerPoint = 0;
1810 
1811 	for (var i = 1; i < this.points.length-1; i++){
1812 		if (this.points[i].distance(this.points[markerPoint]) >= tolerance)
1813 		{
1814 			reduced[reduced.length] = this.points[i];
1815 			markerPoint = i;
1816 		}
1817 	}
1818 
1819 	// Last point
1820 	reduced[reduced.length] = this.points[this.points.length-1];
1821 
1822 	// Revert
1823 	this.points = reduced;
1824 };
1825 
1826 ///////////////
1827 // Radius	//
1828 ///////////////
1829 
1830 /**
1831  * Creates a new radius object for drawing circles around a point, does a lot of initial calculation to increase load time
1832  * @name mxn.Radius
1833  * @constructor
1834  * @param {LatLonPoint} center LatLonPoint of the radius
1835  * @param {Number} quality Number of points that comprise the approximated circle (20 is a good starting point)
1836  * @exports Radius as mxn.Radius
1837  */
1838 var Radius = mxn.Radius = function(center, quality) {
1839 	this.center = center;
1840 	var latConv = center.latConv();
1841 	var lonConv = center.lonConv();
1842 
1843 	// Create Radian conversion constant
1844 	var rad = Math.PI / 180;
1845 	this.calcs = [];
1846 
1847 	for(var i = 0; i < 360; i += quality){
1848 		this.calcs.push([Math.cos(i * rad) / latConv, Math.sin(i * rad) / lonConv]);
1849 	}
1850 };
1851 
1852 /**
1853  * Returns polyline of a circle around the point based on new radius
1854  * @param {Radius} radius
1855  * @param {Color} color
1856  * @returns {Polyline} Polyline
1857  */
1858 Radius.prototype.getPolyline = function(radius, color) {
1859 	var points = [];
1860 
1861 	for(var i = 0; i < this.calcs.length; i++){
1862 		var point = new LatLonPoint(
1863 			this.center.lat + (radius * this.calcs[i][0]),
1864 			this.center.lon + (radius * this.calcs[i][1])
1865 		);
1866 		points.push(point);
1867 	}
1868 	
1869 	// Add first point
1870 	points.push(points[0]);
1871 
1872 	var line = new Polyline(points);
1873 	line.setColor(color);
1874 
1875 	return line;
1876 };
1877 
1878 
1879 })();
1880