      1 // Auto-load scripts
      2 //
      3 // specify which map providers to load by using
      4 // <script src="mxn.js?(provider1,provider2,[module1,module2])" ...
      5 // in your HTML
      6 //
      7 // for each provider mxn.provider.module.js and mxn.module.js will be loaded
      8 // module 'core' is always loaded
      9 //
     10 // NOTE: if you call without providers
     11 // <script src="mxn.js" ...
     12 // no scripts will be loaded at all and it is then up to you to load the scripts independently
     13 (function() {
     14 	var providers = null;
     15 	var modules = 'core';
     16 	var scriptBase;
     17 	var scripts = document.getElementsByTagName('script');
     18
     19 	// Determine which scripts we need to load
     20 	for (var i = 0; i < scripts.length; i++) {
     21 		var match = scripts[i].src.replace(/%20/g , '').match(/^(.*?)mxn\.js(\?\(\[?(.*?)\]?\))?$/);
     22 		if (match !== null) {
     23 			scriptBase = match[1];
     24 			if (match[3]) {
     25 				var settings = match[3].split(',[');
     26 				providers = settings[0].replace(']' , '');
     27 				if (settings[1]) {
     28 					modules += ',' + settings[1];
     29 				}
     30 			}
     31 			break;
     32 	   }
     33 	}
     34
     35 	if (providers === null || providers == 'none') {
     36 		return; // Bail out if no auto-load has been found
     37 	}
     38 	providers = providers.replace(/ /g, '').split(',');
     39 	modules = modules.replace(/ /g, '').split(',');
     40
     41 	// Actually load the scripts
     42 	var scriptTagStart = '<script type="text/javascript" src="' + scriptBase + 'mxn.';
     43 	var scriptTagEnd = '.js"></script>';
     44 	var scriptsAry = [];
     45 	for (i = 0; i < modules.length; i++) {
     46 		scriptsAry.push(scriptTagStart + modules[i] + scriptTagEnd);
     47 		for (var j = 0; j < providers.length; j++) {
     48 			scriptsAry.push(scriptTagStart + providers[j] + '.' + modules[i] + scriptTagEnd);
     49 		}
     50 	}
     51 	document.write(scriptsAry.join(''));
     52 })();
     53
     54 (function(){
     55
     56 // holds all our implementing functions
     57 var apis = {};
     58
     59 // Our special private methods
     60 /**
     61  * Calls the API specific implementation of a particular method.
     62  * Deferrable: If the API implmentation includes a deferable hash such as { getCenter: true, setCenter: true},
     63  * then the methods calls mentioned with in it will be queued until runDeferred is called.
     64  *
     65  * @private
     66  */
     67 var invoke = function(sApiId, sObjName, sFnName, oScope, args){
     68 	if(!hasImplementation(sApiId, sObjName, sFnName)) {
     69 		throw 'Method ' + sFnName + ' of object ' + sObjName + ' is not supported by API ' + sApiId + '. Are you missing a script tag?';
     70 	}
     71 	if(typeof(apis[sApiId][sObjName].deferrable) != 'undefined' && apis[sApiId][sObjName].deferrable[sFnName] === true) {
     72 		mxn.deferUntilLoaded.call(oScope, function() {return apis[sApiId][sObjName][sFnName].apply(oScope, args);} );
     73 	}
     74 	else {
     75 		return apis[sApiId][sObjName][sFnName].apply(oScope, args);
     76 	}
     77 };
     78
     79 /**
     80  * Determines whether the specified API provides an implementation for the
     81  * specified object and function name.
     82  * @private
     83  */
     84 var hasImplementation = function(sApiId, sObjName, sFnName){
     85 	if(typeof(apis[sApiId]) == 'undefined') {
     86 		throw 'API ' + sApiId + ' not loaded. Are you missing a script tag?';
     87 	}
     88 	if(typeof(apis[sApiId][sObjName]) == 'undefined') {
     89 		throw 'Object definition ' + sObjName + ' in API ' + sApiId + ' not loaded. Are you missing a script tag?';
     90 	}
     91 	return typeof(apis[sApiId][sObjName][sFnName]) == 'function';
     92 };
     93
     94 /**
     95  * @name mxn
     96  * @namespace
     97  */
     98 var mxn = window.mxn = /** @lends mxn */ {
     99
    100 	/**
    101 	 * Registers a set of provider specific implementation functions.
    102 	 * @function
    103 	 * @param {String} sApiId The API ID to register implementing functions for.
    104 	 * @param {Object} oApiImpl An object containing the API implementation.
    105 	 */
    106 	register: function(sApiId, oApiImpl){
    107 		if(!apis.hasOwnProperty(sApiId)){
    108 			apis[sApiId] = {};
    109 		}
    110 		mxn.util.merge(apis[sApiId], oApiImpl);
    111 	},
    112
    113 	/**
    114 	 * Adds a list of named proxy methods to the prototype of a
    115 	 * specified constructor function.
    116 	 * @function
    117 	 * @param {Function} func Constructor function to add methods to
    118 	 * @param {Array} aryMethods Array of method names to create
    119 	 * @param {Boolean} bWithApiArg Optional. Whether the proxy methods will use an API argument
    120 	 */
    121 	addProxyMethods: function(func, aryMethods, bWithApiArg){
    122 		for(var i = 0; i < aryMethods.length; i++) {
    123 			var sMethodName = aryMethods[i];
    124 			if(bWithApiArg){
    125 				func.prototype[sMethodName] = new Function('return this.invoker.go(\'' + sMethodName + '\', arguments, { overrideApi: true } );');
    126 			}
    127 			else {
    128 				func.prototype[sMethodName] = new Function('return this.invoker.go(\'' + sMethodName + '\', arguments);');
    129 			}
    130 		}
    131 	},
    132
    133 	checkLoad: function(funcDetails){
    134 		if(this.loaded[this.api] === false) {
    135 			var scope = this;
    136 			this.onload[this.api].push( function() { funcDetails.callee.apply(scope, funcDetails); } );
    137 			return true;
    138 		}
    139 		return false;
    140 	},
    141
    142 	deferUntilLoaded: function(fnCall) {
    143 		if(this.loaded[this.api] === false) {
    144 			var scope = this;
    145 			this.onload[this.api].push( fnCall );
    146 		} else {
    147 			fnCall.call(this);
    148 		}
    149 	},
    150
    151 	/**
    152 	 * Bulk add some named events to an object.
    153 	 * @function
    154 	 * @param {Object} oEvtSrc The event source object.
    155 	 * @param {String[]} aEvtNames Event names to add.
    156 	 */
    157 	addEvents: function(oEvtSrc, aEvtNames){
    158 		for(var i = 0; i < aEvtNames.length; i++){
    159 			var sEvtName = aEvtNames[i];
    160 			if(sEvtName in oEvtSrc){
    161 				throw 'Event or method ' + sEvtName + ' already declared.';
    162 			}
    163 			oEvtSrc[sEvtName] = new mxn.Event(sEvtName, oEvtSrc);
    164 		}
    165 	}
    166
    167 };
    168
    169 /**
    170  * Instantiates a new Event
    171  * @constructor
    172  * @param {String} sEvtName The name of the event.
    173  * @param {Object} oEvtSource The source object of the event.
    174  */
    175 mxn.Event = function(sEvtName, oEvtSource){
    176 	var handlers = [];
    177 	if(!sEvtName){
    178 		throw 'Event name must be provided';
    179 	}
    180 	/**
    181 	 * Add a handler to the Event.
    182 	 * @param {Function} fn The handler function.
    183 	 * @param {Object} ctx The context of the handler function.
    184 	 */
    185 	this.addHandler = function(fn, ctx){
    186 		handlers.push({context: ctx, handler: fn});
    187 	};
    188 	/**
    189 	 * Remove a handler from the Event.
    190 	 * @param {Function} fn The handler function.
    191 	 * @param {Object} ctx The context of the handler function.
    192 	 */
    193 	this.removeHandler = function(fn, ctx){
    194 		for(var i = 0; i < handlers.length; i++){
    195 			if(handlers[i].handler == fn && handlers[i].context == ctx){
    196 				handlers.splice(i, 1);
    197 			}
    198 		}
    199 	};
    200 	/**
    201 	 * Remove all handlers from the Event.
    202 	 */
    203 	this.removeAllHandlers = function(){
    204 		handlers = [];
    205 	};
    206 	/**
    207 	 * Fires the Event.
    208 	 * @param {Object} oEvtArgs Event arguments object to be passed to the handlers.
    209 	 */
    210 	this.fire = function(oEvtArgs){
    211 		var args = [sEvtName, oEvtSource, oEvtArgs];
    212 		for(var i = 0; i < handlers.length; i++){
    213 			handlers[i].handler.apply(handlers[i].context, args);
    214 		}
    215 	};
    216 };
    217
    218 /**
    219  * Creates a new Invoker, a class which helps with on-the-fly
    220  * invocation of the correct API methods.
    221  * @constructor
    222  * @param {Object} aobj The core object whose methods will make cals to go()
    223  * @param {String} asClassName The name of the Mapstraction class to be invoked, normally the same name as aobj's constructor function
    224  * @param {Function} afnApiIdGetter The function on object aobj which will return the active API ID
    225  */
    226 mxn.Invoker = function(aobj, asClassName, afnApiIdGetter){
    227 	var obj = aobj;
    228 	var sClassName = asClassName;
    229 	var fnApiIdGetter = afnApiIdGetter;
    230 	var defOpts = {
    231 		overrideApi: false, // {Boolean} API ID is overridden by value in first argument
    232 		context: null, // {Object} Local vars can be passed from the body of the method to the API method within this object
    233 		fallback: null // {Function} If an API implementation doesn't exist this function is run instead
    234 	};
    235
    236 	/**
    237 	 * Invoke the API implementation of a specific method.
    238 	 * @param {String} sMethodName The method name to invoke
    239 	 * @param {Array} args Arguments to pass on
    240 	 * @param {Object} oOptions Optional. Extra options for invocation
    241 	 * @param {Boolean} oOptions.overrideApi When true the first argument is used as the API ID.
    242 	 * @param {Object} oOptions.context A context object for passing extra information on to the provider implementation.
    243 	 * @param {Function} oOptions.fallback A fallback function to run if the provider implementation is missing.
    244 	 */
    245 	this.go = function(sMethodName, args, oOptions){
    246
    247 		// make sure args is an array
    248 		args = typeof(args) != 'undefined' ? Array.prototype.slice.apply(args) : [];
    249
    250 		if(typeof(oOptions) == 'undefined'){
    251 			oOptions = defOpts;
    252 		}
    253
    254 		var sApiId;
    255 		if(oOptions.overrideApi){
    256 			sApiId = args.shift();
    257 		}
    258 		else {
    259 			sApiId = fnApiIdGetter.apply(obj);
    260 		}
    261
    262 		if(typeof(sApiId) != 'string'){
    263 			throw 'API ID not available.';
    264 		}
    265
    266 		if(typeof(oOptions.context) != 'undefined' && oOptions.context !== null){
    267 			args.push(oOptions.context);
    268 		}
    269
    270 		if(typeof(oOptions.fallback) == 'function' && !hasImplementation(sApiId, sClassName, sMethodName)){
    271 			// we've got no implementation but have got a fallback function
    272 			return oOptions.fallback.apply(obj, args);
    273 		}
    274 		else {
    275 			return invoke(sApiId, sClassName, sMethodName, obj, args);
    276 		}
    277
    278 	};
    279
    280 };
    281
    282 /**
    283  * @namespace
    284  */
    285 mxn.util = {
    286
    287 	/**
    288 	 * Merges properties of one object into another recursively.
    289 	 * @param {Object} oRecv The object receiveing properties
    290 	 * @param {Object} oGive The object donating properties
    291 	 */
    292 	merge: function(oRecv, oGive){
    293 		for (var sPropName in oGive){
    294 			if (oGive.hasOwnProperty(sPropName)) {
    295 				if(!oRecv.hasOwnProperty(sPropName) || typeof(oRecv[sPropName]) !== 'object' || typeof(oGive[sPropName]) !== 'object'){
    296 					oRecv[sPropName] = oGive[sPropName];
    297 				}
    298 				else {
    299 					mxn.util.merge(oRecv[sPropName], oGive[sPropName]);
    300 				}
    301 			}
    302 		}
    303 	},
    304
    305 	/**
    306 	 * $m, the dollar function, elegantising getElementById()
    307 	 * @return An HTML element or array of HTML elements
    308 	 */
    309 	$m: function() {
    310 		var elements = [];
    311 		for (var i = 0; i < arguments.length; i++) {
    312 			var element = arguments[i];
    313 			if (typeof(element) == 'string') {
    314 				element = document.getElementById(element);
    315 			}
    316 			if (arguments.length == 1) {
    317 				return element;
    318 			}
    319 			elements.push(element);
    320 		}
    321 		return elements;
    322 	},
    323
    324 	/**
    325 	 * loadScript is a JSON data fetcher
    326 	 * @param {String} src URL to JSON file
    327 	 * @param {Function} callback Callback function
    328 	 */
    329 	loadScript: function(src, callback) {
    330 		var script = document.createElement('script');
    331 		script.type = 'text/javascript';
    332 		script.src = src;
    333 		if (callback) {
    334 			if(script.addEventListener){
    335 				script.addEventListener('load', callback, true);
    336 			}
    337 			else if(script.attachEvent){
    338 				var done = false;
    339 				script.attachEvent("onreadystatechange",function(){
    340 					if ( !done && document.readyState === "complete" ) {
    341 						done = true;
    342 						callback();
    343 					}
    344 				});
    345 			}
    346 		}
    347 		var h = document.getElementsByTagName('head')[0];
    348 		h.appendChild( script );
    349 		return;
    350 	},
    351
    352 	/**
    353 	 *
    354 	 * @param {Object} point
    355 	 * @param {Object} level
    356 	 */
    357 	convertLatLonXY_Yahoo: function(point, level) { //Mercator
    358 		var size = 1 << (26 - level);
    359 		var pixel_per_degree = size / 360.0;
    360 		var pixel_per_radian = size / (2 * Math.PI);
    361 		var origin = new YCoordPoint(size / 2 , size / 2);
    362 		var answer = new YCoordPoint();
    363 		answer.x = Math.floor(origin.x + point.lon * pixel_per_degree);
    364 		var sin = Math.sin(point.lat * Math.PI / 180.0);
    365 		answer.y = Math.floor(origin.y + 0.5 * Math.log((1 + sin) / (1 - sin)) * -pixel_per_radian);
    366 		return answer;
    367 	},
    368
    369 	/**
    370 	 * Load a stylesheet from a remote file.
    371 	 * @param {String} href URL to the CSS file
    372 	 */
    373 	loadStyle: function(href) {
    374 		var link = document.createElement('link');
    375 		link.type = 'text/css';
    376 		link.rel = 'stylesheet';
    377 		link.href = href;
    378 		document.getElementsByTagName('head')[0].appendChild(link);
    379 		return;
    380 	},
    381
    382 	/**
    383 	 * getStyle provides cross-browser access to css
    384 	 * @param {Object} el HTML Element
    385 	 * @param {String} prop Style property name
    386 	 */
    387 	getStyle: function(el, prop) {
    388 		var y;
    389 		if (el.currentStyle) {
    390 			y = el.currentStyle[prop];
    391 		}
    392 		else if (window.getComputedStyle) {
    393 			y = window.getComputedStyle( el, '').getPropertyValue(prop);
    394 		}
    395 		return y;
    396 	},
    397
    398 	/**
    399 	 * Convert longitude to metres
    400 	 * http://www.uwgb.edu/dutchs/UsefulData/UTMFormulas.HTM
    401 	 * "A degree of longitude at the equator is 111.2km... For other latitudes,
    402 	 * multiply by cos(lat)"
    403 	 * assumes the earth is a sphere but good enough for our purposes
    404 	 * @param {Float} lon
    405 	 * @param {Float} lat
    406 	 */
    407 	lonToMetres: function(lon, lat) {
    408 		return lon * (111200 * Math.cos(lat * (Math.PI / 180)));
    409 	},
    410
    411 	/**
    412 	 * Convert metres to longitude
    413 	 * @param {Object} m
    414 	 * @param {Object} lat
    415 	 */
    416 	metresToLon: function(m, lat) {
    417 		return m / (111200 * Math.cos(lat * (Math.PI / 180)));
    418 	},
    419
    420 	/**
    421 	 * Convert kilometres to miles
    422 	 * @param {Float} km
    423 	 * @returns {Float} miles
    424 	 */
    425 	KMToMiles: function(km) {
    426 		return km / 1.609344;
    427 	},
    428
    429 	/**
    430 	 * Convert miles to kilometres
    431 	 * @param {Float} miles
    432 	 * @returns {Float} km
    433 	 */
    434 	milesToKM: function(miles) {
    435 		return miles * 1.609344;
    436 	},
    437
    438 	// stuff to convert google zoom levels to/from degrees
    439 	// assumes zoom 0 = 256 pixels = 360 degrees
    440 	//		 zoom 1 = 256 pixels = 180 degrees
    441 	// etc.
    442
    443 	/**
    444 	 *
    445 	 * @param {Object} pixels
    446 	 * @param {Object} zoom
    447 	 */
    448 	getDegreesFromGoogleZoomLevel: function(pixels, zoom) {
    449 		return (360 * pixels) / (Math.pow(2, zoom + 8));
    450 	},
    451
    452 	/**
    453 	 *
    454 	 * @param {Object} pixels
    455 	 * @param {Object} degrees
    456 	 */
    457 	getGoogleZoomLevelFromDegrees: function(pixels, degrees) {
    458 		return mxn.util.logN((360 * pixels) / degrees, 2) - 8;
    459 	},
    460
    461 	/**
    462 	 *
    463 	 * @param {Object} number
    464 	 * @param {Object} base
    465 	 */
    466 	logN: function(number, base) {
    467 		return Math.log(number) / Math.log(base);
    468 	},
    469
    470 	/**
    471 	 * Returns array of loaded provider apis
    472 	 * @returns {Array} providers
    473 	 */
    474 	getAvailableProviders : function () {
    475 		var providers = [];
    476 		for (var propertyName in apis){
    477 			if (apis.hasOwnProperty(propertyName)) {
    478 				providers.push(propertyName);
    479 			}
    480 		}
    481 		return providers;
    482 	},
    483
    484 	/**
    485 	 * Formats a string, inserting values of subsequent parameters at specified
    486 	 * locations. e.g. stringFormat('{0} {1}', 'hello', 'world');
    487 	 */
    488 	stringFormat: function(strIn){
    489 		var replaceRegEx = /\{\d+\}/g;
    490 		var args = Array.prototype.slice.apply(arguments);
    491 		args.shift();
    492 		return strIn.replace(replaceRegEx, function(strVal){
    493 			var num = strVal.slice(1, -1);
    494 			return args[num];
    495 		});
    496 	},
    497
    498 	/**
    499 	 * Traverses an object graph using a series of map functions provided as arguments
    500 	 * 2 to n. Map functions are only called if the working object is not undefined/null.
    501 	 * For usage see mxn.google.geocoder.js.
    502 	 */
    503 	traverse: function(start) {
    504 		var args = Array.prototype.slice.apply(arguments);
    505 		args.shift();
    506 		var working = start;
    507 		while(typeof(working) != 'undefined' && working !== null && args.length > 0){
    508 			var op = args.shift();
    509 			working = op(working);
    510 		}
    511 	}
    512 };
    513
    514 /**
    515  * Class for converting between HTML and RGB integer color formats.
    516  * Accepts either a HTML color string argument or three integers for R, G and B.
    517  * @constructor
    518  */
    519 mxn.util.Color = function() {
    520 	if(arguments.length == 3) {
    521 		this.red = arguments[0];
    522 		this.green = arguments[1];
    523 		this.blue = arguments[2];
    524 	}
    525 	else if(arguments.length == 1) {
    526 		this.setHexColor(arguments[0]);
    527 	}
    528 };
    529
    530 mxn.util.Color.prototype.reHex = /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
    531
    532 /**
    533  * Set the color from the supplied HTML hex string.
    534  * @param {String} strHexColor A HTML hex color string e.g. '#00FF88'.
    535  */
    536 mxn.util.Color.prototype.setHexColor = function(strHexColor) {
    537 	var match = strHexColor.match(this.reHex);
    538 	if(match) {
    539 		// grab the code - strips off the preceding # if there is one
    540 		strHexColor = match[1];
    541 	}
    542 	else {
    543 		throw 'Invalid HEX color format, expected #000, 000, #000000 or 000000';
    544 	}
    545 	// if a three character hex code was provided, double up the values
    546 	if(strHexColor.length == 3) {
    547 		strHexColor = strHexColor.replace(/\w/g, function(str){return str.concat(str);});
    548 	}
    549 	this.red = parseInt(strHexColor.substr(0,2), 16);
    550 	this.green = parseInt(strHexColor.substr(2,2), 16);
    551 	this.blue = parseInt(strHexColor.substr(4,2), 16);
    552 };
    553
    554 /**
    555  * Retrieve the color value as an HTML hex string.
    556  * @returns {String} Format '#00FF88'.
    557  */
    558 mxn.util.Color.prototype.getHexColor = function() {
    559 	var rgb = this.blue | (this.green << 8) | (this.red << 16);
    560 	var hexString = rgb.toString(16).toUpperCase();
    561 	if(hexString.length <  6){
    562 		hexString = '0' + hexString;
    563 	}
    564 	return '#' + hexString;
    565 };
    566
    567 })();
    568