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