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