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