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 = 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)){
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.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 
500 /**
501  * Class for converting between HTML and RGB integer color formats.
502  * Accepts either a HTML color string argument or three integers for R, G and B.
503  * @constructor
504  */
505 mxn.util.Color = function() {
506 	if(arguments.length == 3) {
507 		this.red = arguments[0];
508 		this.green = arguments[1];
509 		this.blue = arguments[2];
510 	}
511 	else if(arguments.length == 1) {
512 		this.setHexColor(arguments[0]);
513 	}
514 };
515 
516 mxn.util.Color.prototype.reHex = /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
517 
518 /**
519  * Set the color from the supplied HTML hex string.
520  * @param {String} strHexColor A HTML hex color string e.g. '#00FF88'.
521  */
522 mxn.util.Color.prototype.setHexColor = function(strHexColor) {
523 	var match = strHexColor.match(this.reHex);
524 	if(match) {
525 		// grab the code - strips off the preceding # if there is one
526 		strHexColor = match[1];
527 	}
528 	else {
529 		throw 'Invalid HEX color format, expected #000, 000, #000000 or 000000';
530 	}
531 	// if a three character hex code was provided, double up the values
532 	if(strHexColor.length == 3) {
533 		strHexColor = strHexColor.replace(/\w/g, function(str){return str.concat(str);});
534 	}
535 	this.red = parseInt(strHexColor.substr(0,2), 16);
536 	this.green = parseInt(strHexColor.substr(2,2), 16);
537 	this.blue = parseInt(strHexColor.substr(4,2), 16);
538 };
539 
540 /**
541  * Retrieve the color value as an HTML hex string.
542  * @returns {String} Format '#00FF88'.
543  */
544 mxn.util.Color.prototype.getHexColor = function() {
545 	var rgb = this.blue | (this.green << 8) | (this.red << 16);
546 	var hexString = rgb.toString(16).toUpperCase();
547 	if(hexString.length <  6){
548 		hexString = '0' + hexString;
549 	}
550 	return '#' + hexString;
551 };
552 	
553 })();
554