src/pyams_content/skin/resources/js/jquery-imagemapster-1.2.10.js
changeset 1070 ea0c7ac589c4
parent 1069 abd11be23718
child 1071 e202798d3732
equal deleted inserted replaced
1069:abd11be23718 1070:ea0c7ac589c4
     1 /* ImageMapster
       
     2    Version: 1.2.10 (2/25/2013)
       
     3 
       
     4 Copyright 2011-2012 James Treworgy
       
     5 
       
     6 http://www.outsharked.com/imagemapster
       
     7 https://github.com/jamietre/ImageMapster
       
     8 
       
     9 A jQuery plugin to enhance image maps.
       
    10 
       
    11 */
       
    12 
       
    13 ;
       
    14 
       
    15 /// LICENSE (MIT License)
       
    16 ///
       
    17 /// Permission is hereby granted, free of charge, to any person obtaining
       
    18 /// a copy of this software and associated documentation files (the
       
    19 /// "Software"), to deal in the Software without restriction, including
       
    20 /// without limitation the rights to use, copy, modify, merge, publish,
       
    21 /// distribute, sublicense, and/or sell copies of the Software, and to
       
    22 /// permit persons to whom the Software is furnished to do so, subject to
       
    23 /// the following conditions:
       
    24 ///
       
    25 /// The above copyright notice and this permission notice shall be
       
    26 /// included in all copies or substantial portions of the Software.
       
    27 ///
       
    28 /// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
       
    29 /// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
       
    30 /// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
       
    31 /// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
       
    32 /// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       
    33 /// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
       
    34 /// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       
    35 ///
       
    36 /// January 19, 2011
       
    37 
       
    38 /** @license MIT License (c) copyright B Cavalier & J Hann */
       
    39 
       
    40 /**
       
    41 * when
       
    42 * A lightweight CommonJS Promises/A and when() implementation
       
    43 *
       
    44 * when is part of the cujo.js family of libraries (http://cujojs.com/)
       
    45 *
       
    46 * Licensed under the MIT License at:
       
    47 * http://www.opensource.org/licenses/mit-license.php
       
    48 *
       
    49 * @version 1.2.0
       
    50 */
       
    51 
       
    52 /*lint-ignore-start*/
       
    53 
       
    54 (function (define) {
       
    55     define(function () {
       
    56         var freeze, reduceArray, slice, undef;
       
    57 
       
    58         //
       
    59         // Public API
       
    60         //
       
    61 
       
    62         when.defer = defer;
       
    63         when.reject = reject;
       
    64         when.isPromise = isPromise;
       
    65 
       
    66         when.all = all;
       
    67         when.some = some;
       
    68         when.any = any;
       
    69 
       
    70         when.map = map;
       
    71         when.reduce = reduce;
       
    72 
       
    73         when.chain = chain;
       
    74 
       
    75         /** Object.freeze */
       
    76         freeze = Object.freeze || function (o) { return o; };
       
    77 
       
    78         /**
       
    79         * Trusted Promise constructor.  A Promise created from this constructor is
       
    80         * a trusted when.js promise.  Any other duck-typed promise is considered
       
    81         * untrusted.
       
    82         *
       
    83         * @constructor
       
    84         */
       
    85         function Promise() { }
       
    86 
       
    87         Promise.prototype = freeze({
       
    88             always: function (alwaysback, progback) {
       
    89                 return this.then(alwaysback, alwaysback, progback);
       
    90             },
       
    91 
       
    92             otherwise: function (errback) {
       
    93                 return this.then(undef, errback);
       
    94             }
       
    95         });
       
    96 
       
    97         /**
       
    98         * Create an already-resolved promise for the supplied value
       
    99         * @private
       
   100         *
       
   101         * @param value anything
       
   102         * @return {Promise}
       
   103         */
       
   104         function resolved(value) {
       
   105 
       
   106             var p = new Promise();
       
   107 
       
   108             p.then = function (callback) {
       
   109                 var nextValue;
       
   110                 try {
       
   111                     if (callback) nextValue = callback(value);
       
   112                     return promise(nextValue === undef ? value : nextValue);
       
   113                 } catch (e) {
       
   114                     return rejected(e);
       
   115                 }
       
   116             };
       
   117 
       
   118             return freeze(p);
       
   119         }
       
   120 
       
   121         /**
       
   122         * Create an already-rejected {@link Promise} with the supplied
       
   123         * rejection reason.
       
   124         * @private
       
   125         *
       
   126         * @param reason rejection reason
       
   127         * @return {Promise}
       
   128         */
       
   129         function rejected(reason) {
       
   130 
       
   131             var p = new Promise();
       
   132 
       
   133             p.then = function (callback, errback) {
       
   134                 var nextValue;
       
   135                 try {
       
   136                     if (errback) {
       
   137                         nextValue = errback(reason);
       
   138                         return promise(nextValue === undef ? reason : nextValue)
       
   139                     }
       
   140 
       
   141                     return rejected(reason);
       
   142 
       
   143                 } catch (e) {
       
   144                     return rejected(e);
       
   145                 }
       
   146             };
       
   147 
       
   148             return freeze(p);
       
   149         }
       
   150 
       
   151         /**
       
   152         * Returns a rejected promise for the supplied promiseOrValue. If
       
   153         * promiseOrValue is a value, it will be the rejection value of the
       
   154         * returned promise.  If promiseOrValue is a promise, its
       
   155         * completion value will be the rejected value of the returned promise
       
   156         *
       
   157         * @param promiseOrValue {*} the rejected value of the returned {@link Promise}
       
   158         *
       
   159         * @return {Promise} rejected {@link Promise}
       
   160         */
       
   161         function reject(promiseOrValue) {
       
   162             return when(promiseOrValue, function (value) {
       
   163                 return rejected(value);
       
   164             });
       
   165         }
       
   166 
       
   167         /**
       
   168         * Creates a new, CommonJS compliant, Deferred with fully isolated
       
   169         * resolver and promise parts, either or both of which may be given out
       
   170         * safely to consumers.
       
   171         * The Deferred itself has the full API: resolve, reject, progress, and
       
   172         * then. The resolver has resolve, reject, and progress.  The promise
       
   173         * only has then.
       
   174         *
       
   175         * @memberOf when
       
   176         * @function
       
   177         *
       
   178         * @returns {Deferred}
       
   179         */
       
   180         function defer() {
       
   181             var deferred, promise, listeners, progressHandlers, _then, _progress, complete;
       
   182 
       
   183             listeners = [];
       
   184             progressHandlers = [];
       
   185 
       
   186             /**
       
   187             * Pre-resolution then() that adds the supplied callback, errback, and progback
       
   188             * functions to the registered listeners
       
   189             *
       
   190             * @private
       
   191             *
       
   192             * @param [callback] {Function} resolution handler
       
   193             * @param [errback] {Function} rejection handler
       
   194             * @param [progback] {Function} progress handler
       
   195             *
       
   196             * @throws {Error} if any argument is not null, undefined, or a Function
       
   197             */
       
   198             _then = function unresolvedThen(callback, errback, progback) {
       
   199                 var deferred = defer();
       
   200 
       
   201                 listeners.push(function (promise) {
       
   202                     promise.then(callback, errback)
       
   203 					.then(deferred.resolve, deferred.reject, deferred.progress);
       
   204                 });
       
   205 
       
   206                 progback && progressHandlers.push(progback);
       
   207 
       
   208                 return deferred.promise;
       
   209             };
       
   210 
       
   211             /**
       
   212             * Registers a handler for this {@link Deferred}'s {@link Promise}.  Even though all arguments
       
   213             * are optional, each argument that *is* supplied must be null, undefined, or a Function.
       
   214             * Any other value will cause an Error to be thrown.
       
   215             *
       
   216             * @memberOf Promise
       
   217             *
       
   218             * @param [callback] {Function} resolution handler
       
   219             * @param [errback] {Function} rejection handler
       
   220             * @param [progback] {Function} progress handler
       
   221             *
       
   222             * @throws {Error} if any argument is not null, undefined, or a Function
       
   223             */
       
   224             function then(callback, errback, progback) {
       
   225                 return _then(callback, errback, progback);
       
   226             }
       
   227 
       
   228             /**
       
   229             * Resolves this {@link Deferred}'s {@link Promise} with val as the
       
   230             * resolution value.
       
   231             *
       
   232             * @memberOf Resolver
       
   233             *
       
   234             * @param val anything
       
   235             */
       
   236             function resolve(val) {
       
   237                 complete(resolved(val));
       
   238             }
       
   239 
       
   240             /**
       
   241             * Rejects this {@link Deferred}'s {@link Promise} with err as the
       
   242             * reason.
       
   243             *
       
   244             * @memberOf Resolver
       
   245             *
       
   246             * @param err anything
       
   247             */
       
   248             function reject(err) {
       
   249                 complete(rejected(err));
       
   250             }
       
   251 
       
   252             /**
       
   253             * @private
       
   254             * @param update
       
   255             */
       
   256             _progress = function (update) {
       
   257                 var progress, i = 0;
       
   258                 while (progress = progressHandlers[i++]) progress(update);
       
   259             };
       
   260 
       
   261             /**
       
   262             * Emits a progress update to all progress observers registered with
       
   263             * this {@link Deferred}'s {@link Promise}
       
   264             *
       
   265             * @memberOf Resolver
       
   266             *
       
   267             * @param update anything
       
   268             */
       
   269             function progress(update) {
       
   270                 _progress(update);
       
   271             }
       
   272 
       
   273             /**
       
   274             * Transition from pre-resolution state to post-resolution state, notifying
       
   275             * all listeners of the resolution or rejection
       
   276             *
       
   277             * @private
       
   278             *
       
   279             * @param completed {Promise} the completed value of this deferred
       
   280             */
       
   281             complete = function (completed) {
       
   282                 var listener, i = 0;
       
   283 
       
   284                 // Replace _then with one that directly notifies with the result.
       
   285                 _then = completed.then;
       
   286 
       
   287                 // Replace complete so that this Deferred can only be completed
       
   288                 // once. Also Replace _progress, so that subsequent attempts to issue
       
   289                 // progress throw.
       
   290                 complete = _progress = function alreadyCompleted() {
       
   291                     // TODO: Consider silently returning here so that parties who
       
   292                     // have a reference to the resolver cannot tell that the promise
       
   293                     // has been resolved using try/catch
       
   294                     throw new Error("already completed");
       
   295                 };
       
   296 
       
   297                 // Free progressHandlers array since we'll never issue progress events
       
   298                 // for this promise again now that it's completed
       
   299                 progressHandlers = undef;
       
   300 
       
   301                 // Notify listeners
       
   302                 // Traverse all listeners registered directly with this Deferred
       
   303 
       
   304                 while (listener = listeners[i++]) {
       
   305                     listener(completed);
       
   306                 }
       
   307 
       
   308                 listeners = [];
       
   309             };
       
   310 
       
   311             /**
       
   312             * The full Deferred object, with both {@link Promise} and {@link Resolver}
       
   313             * parts
       
   314             * @class Deferred
       
   315             * @name Deferred
       
   316             */
       
   317             deferred = {};
       
   318 
       
   319             // Promise and Resolver parts
       
   320             // Freeze Promise and Resolver APIs
       
   321 
       
   322             promise = new Promise();
       
   323             promise.then = deferred.then = then;
       
   324 
       
   325             /**
       
   326             * The {@link Promise} for this {@link Deferred}
       
   327             * @memberOf Deferred
       
   328             * @name promise
       
   329             * @type {Promise}
       
   330             */
       
   331             deferred.promise = freeze(promise);
       
   332 
       
   333             /**
       
   334             * The {@link Resolver} for this {@link Deferred}
       
   335             * @memberOf Deferred
       
   336             * @name resolver
       
   337             * @class Resolver
       
   338             */
       
   339             deferred.resolver = freeze({
       
   340                 resolve: (deferred.resolve = resolve),
       
   341                 reject: (deferred.reject = reject),
       
   342                 progress: (deferred.progress = progress)
       
   343             });
       
   344 
       
   345             return deferred;
       
   346         }
       
   347 
       
   348         /**
       
   349         * Determines if promiseOrValue is a promise or not.  Uses the feature
       
   350         * test from http://wiki.commonjs.org/wiki/Promises/A to determine if
       
   351         * promiseOrValue is a promise.
       
   352         *
       
   353         * @param promiseOrValue anything
       
   354         *
       
   355         * @returns {Boolean} true if promiseOrValue is a {@link Promise}
       
   356         */
       
   357         function isPromise(promiseOrValue) {
       
   358             return promiseOrValue && typeof promiseOrValue.then === 'function';
       
   359         }
       
   360 
       
   361         /**
       
   362         * Register an observer for a promise or immediate value.
       
   363         *
       
   364         * @function
       
   365         * @name when
       
   366         * @namespace
       
   367         *
       
   368         * @param promiseOrValue anything
       
   369         * @param {Function} [callback] callback to be called when promiseOrValue is
       
   370         *   successfully resolved.  If promiseOrValue is an immediate value, callback
       
   371         *   will be invoked immediately.
       
   372         * @param {Function} [errback] callback to be called when promiseOrValue is
       
   373         *   rejected.
       
   374         * @param {Function} [progressHandler] callback to be called when progress updates
       
   375         *   are issued for promiseOrValue.
       
   376         *
       
   377         * @returns {Promise} a new {@link Promise} that will complete with the return
       
   378         *   value of callback or errback or the completion value of promiseOrValue if
       
   379         *   callback and/or errback is not supplied.
       
   380         */
       
   381         function when(promiseOrValue, callback, errback, progressHandler) {
       
   382             // Get a promise for the input promiseOrValue
       
   383             // See promise()
       
   384             var trustedPromise = promise(promiseOrValue);
       
   385 
       
   386             // Register promise handlers
       
   387             return trustedPromise.then(callback, errback, progressHandler);
       
   388         }
       
   389 
       
   390         /**
       
   391         * Returns promiseOrValue if promiseOrValue is a {@link Promise}, a new Promise if
       
   392         * promiseOrValue is a foreign promise, or a new, already-resolved {@link Promise}
       
   393         * whose resolution value is promiseOrValue if promiseOrValue is an immediate value.
       
   394         *
       
   395         * Note that this function is not safe to export since it will return its
       
   396         * input when promiseOrValue is a {@link Promise}
       
   397         *
       
   398         * @private
       
   399         *
       
   400         * @param promiseOrValue anything
       
   401         *
       
   402         * @returns Guaranteed to return a trusted Promise.  If promiseOrValue is a when.js {@link Promise}
       
   403         *   returns promiseOrValue, otherwise, returns a new, already-resolved, when.js {@link Promise}
       
   404         *   whose resolution value is:
       
   405         *   * the resolution value of promiseOrValue if it's a foreign promise, or
       
   406         *   * promiseOrValue if it's a value
       
   407         */
       
   408         function promise(promiseOrValue) {
       
   409             var promise, deferred;
       
   410 
       
   411             if (promiseOrValue instanceof Promise) {
       
   412                 // It's a when.js promise, so we trust it
       
   413                 promise = promiseOrValue;
       
   414 
       
   415             } else {
       
   416                 // It's not a when.js promise.  Check to see if it's a foreign promise
       
   417                 // or a value.
       
   418 
       
   419                 deferred = defer();
       
   420                 if (isPromise(promiseOrValue)) {
       
   421                     // It's a compliant promise, but we don't know where it came from,
       
   422                     // so we don't trust its implementation entirely.  Introduce a trusted
       
   423                     // middleman when.js promise
       
   424 
       
   425                     // IMPORTANT: This is the only place when.js should ever call .then() on
       
   426                     // an untrusted promise.
       
   427                     promiseOrValue.then(deferred.resolve, deferred.reject, deferred.progress);
       
   428                     promise = deferred.promise;
       
   429 
       
   430                 } else {
       
   431                     // It's a value, not a promise.  Create an already-resolved promise
       
   432                     // for it.
       
   433                     deferred.resolve(promiseOrValue);
       
   434                     promise = deferred.promise;
       
   435                 }
       
   436             }
       
   437 
       
   438             return promise;
       
   439         }
       
   440 
       
   441         /**
       
   442         * Return a promise that will resolve when howMany of the supplied promisesOrValues
       
   443         * have resolved. The resolution value of the returned promise will be an array of
       
   444         * length howMany containing the resolutions values of the triggering promisesOrValues.
       
   445         *
       
   446         * @memberOf when
       
   447         *
       
   448         * @param promisesOrValues {Array} array of anything, may contain a mix
       
   449         *      of {@link Promise}s and values
       
   450         * @param howMany
       
   451         * @param [callback]
       
   452         * @param [errback]
       
   453         * @param [progressHandler]
       
   454         *
       
   455         * @returns {Promise}
       
   456         */
       
   457         function some(promisesOrValues, howMany, callback, errback, progressHandler) {
       
   458 
       
   459             checkCallbacks(2, arguments);
       
   460 
       
   461             return when(promisesOrValues, function (promisesOrValues) {
       
   462 
       
   463                 var toResolve, results, ret, deferred, resolver, rejecter, handleProgress, len, i;
       
   464 
       
   465                 len = promisesOrValues.length >>> 0;
       
   466 
       
   467                 toResolve = Math.max(0, Math.min(howMany, len));
       
   468                 results = [];
       
   469                 deferred = defer();
       
   470                 ret = when(deferred, callback, errback, progressHandler);
       
   471 
       
   472                 // Wrapper so that resolver can be replaced
       
   473                 function resolve(val) {
       
   474                     resolver(val);
       
   475                 }
       
   476 
       
   477                 // Wrapper so that rejecter can be replaced
       
   478                 function reject(err) {
       
   479                     rejecter(err);
       
   480                 }
       
   481 
       
   482                 // Wrapper so that progress can be replaced
       
   483                 function progress(update) {
       
   484                     handleProgress(update);
       
   485                 }
       
   486 
       
   487                 function complete() {
       
   488                     resolver = rejecter = handleProgress = noop;
       
   489                 }
       
   490 
       
   491                 // No items in the input, resolve immediately
       
   492                 if (!toResolve) {
       
   493                     deferred.resolve(results);
       
   494 
       
   495                 } else {
       
   496                     // Resolver for promises.  Captures the value and resolves
       
   497                     // the returned promise when toResolve reaches zero.
       
   498                     // Overwrites resolver var with a noop once promise has
       
   499                     // be resolved to cover case where n < promises.length
       
   500                     resolver = function (val) {
       
   501                         // This orders the values based on promise resolution order
       
   502                         // Another strategy would be to use the original position of
       
   503                         // the corresponding promise.
       
   504                         results.push(val);
       
   505 
       
   506                         if (! --toResolve) {
       
   507                             complete();
       
   508                             deferred.resolve(results);
       
   509                         }
       
   510                     };
       
   511 
       
   512                     // Rejecter for promises.  Rejects returned promise
       
   513                     // immediately, and overwrites rejecter var with a noop
       
   514                     // once promise to cover case where n < promises.length.
       
   515                     // TODO: Consider rejecting only when N (or promises.length - N?)
       
   516                     // promises have been rejected instead of only one?
       
   517                     rejecter = function (err) {
       
   518                         complete();
       
   519                         deferred.reject(err);
       
   520                     };
       
   521 
       
   522                     handleProgress = deferred.progress;
       
   523 
       
   524                     // TODO: Replace while with forEach
       
   525                     for (i = 0; i < len; ++i) {
       
   526                         if (i in promisesOrValues) {
       
   527                             when(promisesOrValues[i], resolve, reject, progress);
       
   528                         }
       
   529                     }
       
   530                 }
       
   531 
       
   532                 return ret;
       
   533             });
       
   534         }
       
   535 
       
   536         /**
       
   537         * Return a promise that will resolve only once all the supplied promisesOrValues
       
   538         * have resolved. The resolution value of the returned promise will be an array
       
   539         * containing the resolution values of each of the promisesOrValues.
       
   540         *
       
   541         * @memberOf when
       
   542         *
       
   543         * @param promisesOrValues {Array|Promise} array of anything, may contain a mix
       
   544         *      of {@link Promise}s and values
       
   545         * @param [callback] {Function}
       
   546         * @param [errback] {Function}
       
   547         * @param [progressHandler] {Function}
       
   548         *
       
   549         * @returns {Promise}
       
   550         */
       
   551         function all(promisesOrValues, callback, errback, progressHandler) {
       
   552 
       
   553             checkCallbacks(1, arguments);
       
   554 
       
   555             return when(promisesOrValues, function (promisesOrValues) {
       
   556                 return _reduce(promisesOrValues, reduceIntoArray, []);
       
   557             }).then(callback, errback, progressHandler);
       
   558         }
       
   559 
       
   560         function reduceIntoArray(current, val, i) {
       
   561             current[i] = val;
       
   562             return current;
       
   563         }
       
   564 
       
   565         /**
       
   566         * Return a promise that will resolve when any one of the supplied promisesOrValues
       
   567         * has resolved. The resolution value of the returned promise will be the resolution
       
   568         * value of the triggering promiseOrValue.
       
   569         *
       
   570         * @memberOf when
       
   571         *
       
   572         * @param promisesOrValues {Array|Promise} array of anything, may contain a mix
       
   573         *      of {@link Promise}s and values
       
   574         * @param [callback] {Function}
       
   575         * @param [errback] {Function}
       
   576         * @param [progressHandler] {Function}
       
   577         *
       
   578         * @returns {Promise}
       
   579         */
       
   580         function any(promisesOrValues, callback, errback, progressHandler) {
       
   581 
       
   582             function unwrapSingleResult(val) {
       
   583                 return callback ? callback(val[0]) : val[0];
       
   584             }
       
   585 
       
   586             return some(promisesOrValues, 1, unwrapSingleResult, errback, progressHandler);
       
   587         }
       
   588 
       
   589         /**
       
   590         * Traditional map function, similar to `Array.prototype.map()`, but allows
       
   591         * input to contain {@link Promise}s and/or values, and mapFunc may return
       
   592         * either a value or a {@link Promise}
       
   593         *
       
   594         * @memberOf when
       
   595         *
       
   596         * @param promise {Array|Promise} array of anything, may contain a mix
       
   597         *      of {@link Promise}s and values
       
   598         * @param mapFunc {Function} mapping function mapFunc(value) which may return
       
   599         *      either a {@link Promise} or value
       
   600         *
       
   601         * @returns {Promise} a {@link Promise} that will resolve to an array containing
       
   602         *      the mapped output values.
       
   603         */
       
   604         function map(promise, mapFunc) {
       
   605             return when(promise, function (array) {
       
   606                 return _map(array, mapFunc);
       
   607             });
       
   608         }
       
   609 
       
   610         /**
       
   611         * Private map helper to map an array of promises
       
   612         * @private
       
   613         *
       
   614         * @param promisesOrValues {Array}
       
   615         * @param mapFunc {Function}
       
   616         * @return {Promise}
       
   617         */
       
   618         function _map(promisesOrValues, mapFunc) {
       
   619 
       
   620             var results, len, i;
       
   621 
       
   622             // Since we know the resulting length, we can preallocate the results
       
   623             // array to avoid array expansions.
       
   624             len = promisesOrValues.length >>> 0;
       
   625             results = new Array(len);
       
   626 
       
   627             // Since mapFunc may be async, get all invocations of it into flight
       
   628             // asap, and then use reduce() to collect all the results
       
   629             for (i = 0; i < len; i++) {
       
   630                 if (i in promisesOrValues)
       
   631                     results[i] = when(promisesOrValues[i], mapFunc);
       
   632             }
       
   633 
       
   634             // Could use all() here, but that would result in another array
       
   635             // being allocated, i.e. map() would end up allocating 2 arrays
       
   636             // of size len instead of just 1.  Since all() uses reduce()
       
   637             // anyway, avoid the additional allocation by calling reduce
       
   638             // directly.
       
   639             return _reduce(results, reduceIntoArray, results);
       
   640         }
       
   641 
       
   642         /**
       
   643         * Traditional reduce function, similar to `Array.prototype.reduce()`, but
       
   644         * input may contain {@link Promise}s and/or values, and reduceFunc
       
   645         * may return either a value or a {@link Promise}, *and* initialValue may
       
   646         * be a {@link Promise} for the starting value.
       
   647         *
       
   648         * @memberOf when
       
   649         *
       
   650         * @param promise {Array|Promise} array of anything, may contain a mix
       
   651         *      of {@link Promise}s and values.  May also be a {@link Promise} for
       
   652         *      an array.
       
   653         * @param reduceFunc {Function} reduce function reduce(currentValue, nextValue, index, total),
       
   654         *      where total is the total number of items being reduced, and will be the same
       
   655         *      in each call to reduceFunc.
       
   656         * @param initialValue starting value, or a {@link Promise} for the starting value
       
   657         *
       
   658         * @returns {Promise} that will resolve to the final reduced value
       
   659         */
       
   660         function reduce(promise, reduceFunc, initialValue) {
       
   661             var args = slice.call(arguments, 1);
       
   662             return when(promise, function (array) {
       
   663                 return _reduce.apply(undef, [array].concat(args));
       
   664             });
       
   665         }
       
   666 
       
   667         /**
       
   668         * Private reduce to reduce an array of promises
       
   669         * @private
       
   670         *
       
   671         * @param promisesOrValues {Array}
       
   672         * @param reduceFunc {Function}
       
   673         * @param initialValue {*}
       
   674         * @return {Promise}
       
   675         */
       
   676         function _reduce(promisesOrValues, reduceFunc, initialValue) {
       
   677 
       
   678             var total, args;
       
   679 
       
   680             total = promisesOrValues.length;
       
   681 
       
   682             // Skip promisesOrValues, since it will be used as 'this' in the call
       
   683             // to the actual reduce engine below.
       
   684 
       
   685             // Wrap the supplied reduceFunc with one that handles promises and then
       
   686             // delegates to the supplied.
       
   687 
       
   688             args = [
       
   689 			function (current, val, i) {
       
   690 			    return when(current, function (c) {
       
   691 			        return when(val, function (value) {
       
   692 			            return reduceFunc(c, value, i, total);
       
   693 			        });
       
   694 			    });
       
   695 			}
       
   696 		];
       
   697 
       
   698             if (arguments.length > 2) args.push(initialValue);
       
   699 
       
   700             return reduceArray.apply(promisesOrValues, args);
       
   701         }
       
   702 
       
   703         /**
       
   704         * Ensure that resolution of promiseOrValue will complete resolver with the completion
       
   705         * value of promiseOrValue, or instead with resolveValue if it is provided.
       
   706         *
       
   707         * @memberOf when
       
   708         *
       
   709         * @param promiseOrValue
       
   710         * @param resolver {Resolver}
       
   711         * @param [resolveValue] anything
       
   712         *
       
   713         * @returns {Promise}
       
   714         */
       
   715         function chain(promiseOrValue, resolver, resolveValue) {
       
   716             var useResolveValue = arguments.length > 2;
       
   717 
       
   718             return when(promiseOrValue,
       
   719 			function (val) {
       
   720 			    if (useResolveValue) val = resolveValue;
       
   721 			    resolver.resolve(val);
       
   722 			    return val;
       
   723 			},
       
   724 			function (e) {
       
   725 			    resolver.reject(e);
       
   726 			    return rejected(e);
       
   727 			},
       
   728 			resolver.progress
       
   729 		);
       
   730         }
       
   731 
       
   732         //
       
   733         // Utility functions
       
   734         //
       
   735 
       
   736         /**
       
   737         * Helper that checks arrayOfCallbacks to ensure that each element is either
       
   738         * a function, or null or undefined.
       
   739         *
       
   740         * @private
       
   741         *
       
   742         * @param arrayOfCallbacks {Array} array to check
       
   743         * @throws {Error} if any element of arrayOfCallbacks is something other than
       
   744         * a Functions, null, or undefined.
       
   745         */
       
   746         function checkCallbacks(start, arrayOfCallbacks) {
       
   747             var arg, i = arrayOfCallbacks.length;
       
   748             while (i > start) {
       
   749                 arg = arrayOfCallbacks[--i];
       
   750                 if (arg != null && typeof arg != 'function') throw new Error('callback is not a function');
       
   751             }
       
   752         }
       
   753 
       
   754         /**
       
   755         * No-Op function used in method replacement
       
   756         * @private
       
   757         */
       
   758         function noop() { }
       
   759 
       
   760         slice = [].slice;
       
   761 
       
   762         // ES5 reduce implementation if native not available
       
   763         // See: http://es5.github.com/#x15.4.4.21 as there are many
       
   764         // specifics and edge cases.
       
   765         reduceArray = [].reduce ||
       
   766 		function (reduceFunc /*, initialValue */) {
       
   767 		    // ES5 dictates that reduce.length === 1
       
   768 
       
   769 		    // This implementation deviates from ES5 spec in the following ways:
       
   770 		    // 1. It does not check if reduceFunc is a Callable
       
   771 
       
   772 		    var arr, args, reduced, len, i;
       
   773 
       
   774 		    i = 0;
       
   775 		    arr = Object(this);
       
   776 		    len = arr.length >>> 0;
       
   777 		    args = arguments;
       
   778 
       
   779 		    // If no initialValue, use first item of array (we know length !== 0 here)
       
   780 		    // and adjust i to start at second item
       
   781 		    if (args.length <= 1) {
       
   782 		        // Skip to the first real element in the array
       
   783 		        for (; ; ) {
       
   784 		            if (i in arr) {
       
   785 		                reduced = arr[i++];
       
   786 		                break;
       
   787 		            }
       
   788 
       
   789 		            // If we reached the end of the array without finding any real
       
   790 		            // elements, it's a TypeError
       
   791 		            if (++i >= len) {
       
   792 		                throw new TypeError();
       
   793 		            }
       
   794 		        }
       
   795 		    } else {
       
   796 		        // If initialValue provided, use it
       
   797 		        reduced = args[1];
       
   798 		    }
       
   799 
       
   800 		    // Do the actual reduce
       
   801 		    for (; i < len; ++i) {
       
   802 		        // Skip holes
       
   803 		        if (i in arr)
       
   804 		            reduced = reduceFunc(reduced, arr[i], i, arr);
       
   805 		    }
       
   806 
       
   807 		    return reduced;
       
   808 		};
       
   809 
       
   810         return when;
       
   811     });
       
   812 })(typeof define == 'function'
       
   813 	? define
       
   814 	: function (factory) {
       
   815 	    typeof module != 'undefined'
       
   816 		? (module.exports = factory())
       
   817 		: (jQuery.mapster_when = factory());
       
   818 	}
       
   819 // Boilerplate for AMD, Node, and browser global
       
   820 );
       
   821 /*lint-ignore-end*/
       
   822 /* ImageMapster core */
       
   823 
       
   824 /*jslint laxbreak: true, evil: true, unparam: true */
       
   825 
       
   826 /*global jQuery: true, Zepto: true */
       
   827 
       
   828 
       
   829 (function ($) {
       
   830     // all public functions in $.mapster.impl are methods
       
   831     $.fn.mapster = function (method) {
       
   832         var m = $.mapster.impl;
       
   833         if ($.isFunction(m[method])) {
       
   834             return m[method].apply(this, Array.prototype.slice.call(arguments, 1));
       
   835         } else if (typeof method === 'object' || !method) {
       
   836             return m.bind.apply(this, arguments);
       
   837         } else {
       
   838             $.error('Method ' + method + ' does not exist on jQuery.mapster');
       
   839         }
       
   840     };
       
   841 
       
   842     $.mapster = {
       
   843         version: "1.2.10",
       
   844         render_defaults: {
       
   845             isSelectable: true,
       
   846             isDeselectable: true,
       
   847             fade: false,
       
   848             fadeDuration: 150,
       
   849             fill: true,
       
   850             fillColor: '000000',
       
   851             fillColorMask: 'FFFFFF',
       
   852             fillOpacity: 0.7,
       
   853             highlight: true,
       
   854             stroke: false,
       
   855             strokeColor: 'ff0000',
       
   856             strokeOpacity: 1,
       
   857             strokeWidth: 1,
       
   858             includeKeys: '',
       
   859             altImage: null,
       
   860             altImageId: null, // used internally            
       
   861             altImages: {} 
       
   862         },
       
   863         defaults: {
       
   864             clickNavigate: false,
       
   865             wrapClass: null,
       
   866             wrapCss: null,
       
   867             onGetList: null,
       
   868             sortList: false,
       
   869             listenToList: false,
       
   870             mapKey: '',
       
   871             mapValue: '',
       
   872             singleSelect: false,
       
   873             listKey: 'value',
       
   874             listSelectedAttribute: 'selected',
       
   875             listSelectedClass: null,
       
   876             onClick: null,
       
   877             onMouseover: null,
       
   878             onMouseout: null,
       
   879             mouseoutDelay: 0,
       
   880             onStateChange: null,
       
   881             boundList: null,
       
   882             onConfigured: null,
       
   883             configTimeout: 30000,
       
   884             noHrefIsMask: true,
       
   885             scaleMap: true,
       
   886             safeLoad: false,
       
   887             areas: []
       
   888         },
       
   889         shared_defaults: {
       
   890             render_highlight: { fade: true },
       
   891             render_select: { fade: false },
       
   892             staticState: null,
       
   893             selected: null
       
   894         },
       
   895         area_defaults:
       
   896         {
       
   897             includeKeys: '',
       
   898             isMask: false
       
   899         },
       
   900         canvas_style: {
       
   901             position: 'absolute',
       
   902             left: 0,
       
   903             top: 0,
       
   904             padding: 0,
       
   905             border: 0
       
   906         },
       
   907         hasCanvas: null,
       
   908         isTouch: null,
       
   909         map_cache: [],
       
   910         hooks: {},
       
   911         addHook: function(name,callback) {
       
   912             this.hooks[name]=(this.hooks[name]||[]).push(callback);
       
   913         },
       
   914         callHooks: function(name,context) {
       
   915             $.each(this.hooks[name]||[],function(i,e) {
       
   916                 e.apply(context);
       
   917             });
       
   918         },
       
   919         utils: {
       
   920             when: $.mapster_when,
       
   921             defer: $.mapster_when.defer,
       
   922 
       
   923             // extends the constructor, returns a new object prototype. Does not refer to the
       
   924             // original constructor so is protected if the original object is altered. This way you
       
   925             // can "extend" an object by replacing it with its subclass.
       
   926             subclass: function(BaseClass, constr) {
       
   927                 var Subclass=function() {
       
   928                     var me=this, 
       
   929                         args=Array.prototype.slice.call(arguments,0);
       
   930                     me.base = BaseClass.prototype;
       
   931                     me.base.init = function() {
       
   932                         BaseClass.prototype.constructor.apply(me,args);
       
   933                     };
       
   934                     constr.apply(me,args);
       
   935                 };
       
   936                 Subclass.prototype = new BaseClass();
       
   937                 Subclass.prototype.constructor=Subclass;
       
   938                 return Subclass;
       
   939             },
       
   940             asArray: function (obj) {
       
   941                 return obj.constructor === Array ?
       
   942                     obj : this.split(obj);
       
   943             },
       
   944             // clean split: no padding or empty elements
       
   945             split: function (text,cb) {
       
   946                 var i,el, arr = text.split(',');
       
   947                 for (i = 0; i < arr.length; i++) {
       
   948                     el = $.trim(arr[i]);
       
   949                     if (el==='') {
       
   950                         arr.splice(i,1);
       
   951                     } else {
       
   952                         arr[i] = cb ? cb(el):el;
       
   953                     }
       
   954                 }
       
   955                 return arr;
       
   956             },
       
   957             // similar to $.extend but does not add properties (only updates), unless the
       
   958             // first argument is an empty object, then all properties will be copied
       
   959             updateProps: function (_target, _template) {
       
   960                 var onlyProps,
       
   961                     target = _target || {},
       
   962                     template = $.isEmptyObject(target) ? _template : _target;
       
   963 
       
   964                 //if (template) {
       
   965                 onlyProps = [];
       
   966                 $.each(template, function (prop) {
       
   967                     onlyProps.push(prop);
       
   968                 });
       
   969                 //}
       
   970 
       
   971                 $.each(Array.prototype.slice.call(arguments, 1), function (i, src) {
       
   972                     $.each(src || {}, function (prop) {
       
   973                         if (!onlyProps || $.inArray(prop, onlyProps) >= 0) {
       
   974                             var p = src[prop];
       
   975 
       
   976                             if ($.isPlainObject(p)) {
       
   977                                 // not recursive - only copies 1 level of subobjects, and always merges
       
   978                                 target[prop] = $.extend(target[prop] || {}, p);
       
   979                             } else if (p && p.constructor === Array) {
       
   980                                 target[prop] = p.slice(0);
       
   981                             } else if (typeof p !== 'undefined') {
       
   982                                 target[prop] = src[prop];
       
   983                             }
       
   984 
       
   985                         }
       
   986                     });
       
   987                 });
       
   988                 return target;
       
   989             },
       
   990             isElement: function (o) {
       
   991                 return (typeof HTMLElement === "object" ? o instanceof HTMLElement :
       
   992                         o && typeof o === "object" && o.nodeType === 1 && typeof o.nodeName === "string");
       
   993             },
       
   994             // finds element of array or object with a property "prop" having value "val"
       
   995             // if prop is not defined, then just looks for property with value "val"
       
   996             indexOfProp: function (obj, prop, val) {
       
   997                 var result = obj.constructor === Array ? -1 : null;
       
   998                 $.each(obj, function (i, e) {
       
   999                     if (e && (prop ? e[prop] : e) === val) {
       
  1000                         result = i;
       
  1001                         return false;
       
  1002                     }
       
  1003                 });
       
  1004                 return result;
       
  1005             },
       
  1006             // returns "obj" if true or false, or "def" if not true/false
       
  1007             boolOrDefault: function (obj, def) {
       
  1008                 return this.isBool(obj) ?
       
  1009                         obj : def || false;
       
  1010             },
       
  1011             isBool: function (obj) {
       
  1012                 return typeof obj === "boolean";
       
  1013             },
       
  1014             isUndef: function(obj) {
       
  1015                 return typeof obj === "undefined";
       
  1016             },
       
  1017             // evaluates "obj", if function, calls it with args
       
  1018             // (todo - update this to handle variable lenght/more than one arg)
       
  1019             ifFunction: function (obj, that, args) {
       
  1020                 if ($.isFunction(obj)) {
       
  1021                     obj.call(that, args);
       
  1022                 }
       
  1023             },
       
  1024             size: function(image, raw) {
       
  1025                 var u=$.mapster.utils;
       
  1026                 return { 
       
  1027                     width: raw ? (image.width || image.naturalWidth) : u.imgWidth(image,true) ,
       
  1028                     height: raw ? (image.height || image.naturalHeight) : u.imgHeight(image,true),
       
  1029                     complete: function() { return !!this.height && !!this.width;}
       
  1030                 };
       
  1031             },
       
  1032 
       
  1033                 
       
  1034             /**
       
  1035              * Set the opacity of the element. This is an IE<8 specific function for handling VML.
       
  1036              * When using VML we must override the "setOpacity" utility function (monkey patch ourselves).
       
  1037              * jQuery does not deal with opacity correctly for VML elements. This deals with that.
       
  1038              * 
       
  1039              * @param {Element} el The DOM element
       
  1040              * @param {double} opacity A value between 0 and 1 inclusive.
       
  1041              */
       
  1042 
       
  1043             setOpacity: function (el, opacity) {
       
  1044                 if ($.mapster.hasCanvas()) {
       
  1045                     el.style.opacity = opacity;
       
  1046                 } else {
       
  1047                     $(el).each(function(i,e) {
       
  1048                         if (typeof e.opacity !=='undefined') {
       
  1049                            e.opacity=opacity;
       
  1050                         } else {
       
  1051                             $(e).css("opacity",opacity);
       
  1052                         }
       
  1053                     });
       
  1054                 }
       
  1055             },
       
  1056 
       
  1057 
       
  1058             // fade "el" from opacity "op" to "endOp" over a period of time "duration"
       
  1059             
       
  1060             fader: (function () {
       
  1061                 var elements = {},
       
  1062                         lastKey = 0,
       
  1063                         fade_func = function (el, op, endOp, duration) {
       
  1064                             var index, 
       
  1065                                 cbIntervals = duration/15,
       
  1066                                 obj, u = $.mapster.utils;
       
  1067 
       
  1068                             if (typeof el === 'number') {
       
  1069                                 obj = elements[el];
       
  1070                                 if (!obj) {
       
  1071                                     return;
       
  1072                                 }
       
  1073                             } else {
       
  1074                                 index = u.indexOfProp(elements, null, el);
       
  1075                                 if (index) {
       
  1076                                     delete elements[index];
       
  1077                                 }
       
  1078                                 elements[++lastKey] = obj = el;
       
  1079                                 el = lastKey;
       
  1080                             }
       
  1081 
       
  1082                             endOp = endOp || 1;
       
  1083 
       
  1084                             op = (op + (endOp / cbIntervals) > endOp - 0.01) ? endOp : op + (endOp / cbIntervals);
       
  1085 
       
  1086                             u.setOpacity(obj, op);
       
  1087                             if (op < endOp) {
       
  1088                                 setTimeout(function () {
       
  1089                                     fade_func(el, op, endOp, duration);
       
  1090                                 }, 15);
       
  1091                             }
       
  1092                         };
       
  1093                 return fade_func;
       
  1094             } ())
       
  1095         },
       
  1096         getBoundList: function (opts, key_list) {
       
  1097             if (!opts.boundList) {
       
  1098                 return null;
       
  1099             }
       
  1100             var index, key, result = $(), list = $.mapster.utils.split(key_list);
       
  1101             opts.boundList.each(function (i,e) {
       
  1102                 for (index = 0; index < list.length; index++) {
       
  1103                     key = list[index];
       
  1104                     if ($(e).is('[' + opts.listKey + '="' + key + '"]')) {
       
  1105                         result = result.add(e);
       
  1106                     }
       
  1107                 }
       
  1108             });
       
  1109             return result;
       
  1110         },
       
  1111         // Causes changes to the bound list based on the user action (select or deselect)
       
  1112         // area: the jQuery area object
       
  1113         // returns the matching elements from the bound list for the first area passed (normally only one should be passed, but
       
  1114         // a list can be passed
       
  1115         setBoundListProperties: function (opts, target, selected) {
       
  1116             target.each(function (i,e) {
       
  1117                 if (opts.listSelectedClass) {
       
  1118                     if (selected) {
       
  1119                         $(e).addClass(opts.listSelectedClass);
       
  1120                     } else {
       
  1121                         $(e).removeClass(opts.listSelectedClass);
       
  1122                     }
       
  1123                 }
       
  1124                 if (opts.listSelectedAttribute) {
       
  1125                     $(e).attr(opts.listSelectedAttribute, selected);
       
  1126                 }
       
  1127             });
       
  1128         },
       
  1129         getMapDataIndex: function (obj) {
       
  1130             var img, id;
       
  1131             switch (obj.tagName && obj.tagName.toLowerCase()) {
       
  1132                 case 'area':
       
  1133                     id = $(obj).parent().attr('name');
       
  1134                     img = $("img[usemap='#" + id + "']")[0];
       
  1135                     break;
       
  1136                 case 'img':
       
  1137                     img = obj;
       
  1138                     break;
       
  1139             }
       
  1140             return img ?
       
  1141                 this.utils.indexOfProp(this.map_cache, 'image', img) : -1;
       
  1142         },
       
  1143         getMapData: function (obj) {
       
  1144             var index = this.getMapDataIndex(obj.length ? obj[0]:obj);
       
  1145             if (index >= 0) {
       
  1146                 return index >= 0 ? this.map_cache[index] : null;
       
  1147             }
       
  1148         },
       
  1149         /**
       
  1150          * Queue a command to be run after the active async operation has finished
       
  1151          * @param  {MapData}  map_data    The target MapData object
       
  1152          * @param  {jQuery}   that        jQuery object on which the command was invoked
       
  1153          * @param  {string}   command     the ImageMapster method name
       
  1154          * @param  {object[]} args        arguments passed to the method
       
  1155          * @return {bool}                 true if the command was queued, false if not (e.g. there was no need to)
       
  1156          */
       
  1157         queueCommand: function (map_data, that, command, args) {
       
  1158             if (!map_data) {
       
  1159                 return false;
       
  1160             }
       
  1161             if (!map_data.complete || map_data.currentAction) {
       
  1162                 map_data.commands.push(
       
  1163                 {
       
  1164                     that: that,
       
  1165                     command: command,
       
  1166                     args: args
       
  1167                 });
       
  1168                 return true;
       
  1169             }
       
  1170             return false;
       
  1171         },
       
  1172         unload: function () {
       
  1173             this.impl.unload();
       
  1174             this.utils = null;
       
  1175             this.impl = null;
       
  1176             $.fn.mapster = null;
       
  1177             $.mapster = null;
       
  1178             $('*').unbind();
       
  1179         }
       
  1180     };
       
  1181 
       
  1182     // Config for object prototypes
       
  1183     // first: use only first object (for things that should not apply to lists)
       
  1184     /// calls back one of two fuinctions, depending on whether an area was obtained.
       
  1185     // opts: {
       
  1186     //    name: 'method name',
       
  1187     //    key: 'key,
       
  1188     //    args: 'args'
       
  1189     //
       
  1190     //}
       
  1191     // name: name of method (required)
       
  1192     // args: arguments to re-call with
       
  1193     // Iterates through all the objects passed, and determines whether it's an area or an image, and calls the appropriate
       
  1194     // callback for each. If anything is returned from that callback, the process is stopped and that data return. Otherwise,
       
  1195     // the object itself is returned.
       
  1196     
       
  1197     var m = $.mapster, 
       
  1198         u = m.utils,
       
  1199         ap = Array.prototype;
       
  1200 
       
  1201 
       
  1202     // jQuery's width() and height() are broken on IE9 in some situations. This tries everything. 
       
  1203     $.each(["width","height"],function(i,e) {
       
  1204         var capProp = e.substr(0,1).toUpperCase() + e.substr(1);
       
  1205         // when jqwidth parm is passed, it also checks the jQuery width()/height() property
       
  1206         // the issue is that jQUery width() can report a valid size before the image is loaded in some browsers
       
  1207         // without it, we can read zero even when image is loaded in other browsers if its not visible
       
  1208         // we must still check because stuff like adblock can temporarily block it
       
  1209         // what a goddamn headache
       
  1210         u["img"+capProp]=function(img,jqwidth) {
       
  1211                 return (jqwidth ? $(img)[e]() : 0) || 
       
  1212                     img[e] || img["natural"+capProp] || img["client"+capProp] || img["offset"+capProp];
       
  1213         };
       
  1214      
       
  1215     });    
       
  1216 
       
  1217     /**
       
  1218      * The Method object encapsulates the process of testing an ImageMapster method to see if it's being
       
  1219      * invoked on an image, or an area; then queues the command if the MapData is in an active state.
       
  1220      * 
       
  1221      * @param {[jQuery]}    that        The target of the invocation
       
  1222      * @param {[function]}  func_map    The callback if the target is an imagemap
       
  1223      * @param {[function]}  func_area   The callback if the target is an area
       
  1224      * @param {[object]}    opt         Options: { key: a map key if passed explicitly
       
  1225      *                                             name: the command name, if it can be queued,
       
  1226      *                                             args: arguments to the method
       
  1227      *                                            }
       
  1228      */
       
  1229     
       
  1230     m.Method = function (that, func_map, func_area, opts) {
       
  1231         var me = this;
       
  1232         me.name = opts.name;
       
  1233         me.output = that;
       
  1234         me.input = that;
       
  1235         me.first = opts.first || false;
       
  1236         me.args = opts.args ? ap.slice.call(opts.args, 0) : [];
       
  1237         me.key = opts.key;
       
  1238         me.func_map = func_map;
       
  1239         me.func_area = func_area;
       
  1240         //$.extend(me, opts);
       
  1241         me.name = opts.name;
       
  1242         me.allowAsync = opts.allowAsync || false;
       
  1243     };
       
  1244     m.Method.prototype = {
       
  1245         constructor: m.Method,
       
  1246         go: function () {
       
  1247             var i,  data, ar, len, result, src = this.input,
       
  1248                     area_list = [],
       
  1249                     me = this;
       
  1250 
       
  1251             len = src.length;
       
  1252             for (i = 0; i < len; i++) {
       
  1253                 data = $.mapster.getMapData(src[i]);
       
  1254                 if (data) {
       
  1255                     if (!me.allowAsync && m.queueCommand(data, me.input, me.name, me.args)) {
       
  1256                         if (this.first) {
       
  1257                             result = '';
       
  1258                         }
       
  1259                         continue;
       
  1260                     }
       
  1261                     
       
  1262                     ar = data.getData(src[i].nodeName === 'AREA' ? src[i] : this.key);
       
  1263                     if (ar) {
       
  1264                         if ($.inArray(ar, area_list) < 0) {
       
  1265                             area_list.push(ar);
       
  1266                         }
       
  1267                     } else {
       
  1268                         result = this.func_map.apply(data, me.args);
       
  1269                     }
       
  1270                     if (this.first || typeof result !== 'undefined') {
       
  1271                         break;
       
  1272                     }
       
  1273                 }
       
  1274             }
       
  1275             // if there were areas, call the area function for each unique group
       
  1276             $(area_list).each(function (i,e) {
       
  1277                 result = me.func_area.apply(e, me.args);
       
  1278             });
       
  1279 
       
  1280             if (typeof result !== 'undefined') {
       
  1281                 return result;
       
  1282             } else {
       
  1283                 return this.output;
       
  1284             }
       
  1285         }
       
  1286     };
       
  1287 
       
  1288     $.mapster.impl = (function () {
       
  1289         var me = {},
       
  1290         addMap= function (map_data) {
       
  1291             return m.map_cache.push(map_data) - 1;
       
  1292         },
       
  1293         removeMap = function (map_data) {
       
  1294             m.map_cache.splice(map_data.index, 1);
       
  1295             for (var i = m.map_cache.length - 1; i >= this.index; i--) {
       
  1296                 m.map_cache[i].index--;
       
  1297             }
       
  1298         };
       
  1299 
       
  1300         
       
  1301         /**
       
  1302          * Test whether the browser supports VML. Credit: google.
       
  1303          * http://stackoverflow.com/questions/654112/how-do-you-detect-support-for-vml-or-svg-in-a-browser
       
  1304          * 
       
  1305          * @return {bool} true if vml is supported, false if not
       
  1306          */
       
  1307         
       
  1308         function hasVml() {
       
  1309             var a = $('<div />').appendTo('body');
       
  1310             a.html('<v:shape id="vml_flag1" adj="1" />');
       
  1311             
       
  1312             var b = a[0].firstChild;
       
  1313             b.style.behavior = "url(#default#VML)";
       
  1314             var has = b ? typeof b.adj === "object" : true;
       
  1315             a.remove();
       
  1316             return has;
       
  1317         }
       
  1318 
       
  1319         /**
       
  1320          * Return a reference to the IE namespaces object, if available, or an empty object otherwise
       
  1321          * @return {obkect} The document.namespaces object.
       
  1322          */
       
  1323         function namespaces() {
       
  1324             return typeof(document.namespaces)==='object' ?
       
  1325                 document.namespaces :
       
  1326                 null;
       
  1327         }
       
  1328 
       
  1329         /**
       
  1330          * Test for the presence of HTML5 Canvas support. This also checks to see if excanvas.js has been 
       
  1331          * loaded and is faking it; if so, we assume that canvas is not supported.
       
  1332          *
       
  1333          * @return {bool} true if HTML5 canvas support, false if not
       
  1334          */
       
  1335         
       
  1336         function hasCanvas() {
       
  1337             var d = namespaces();
       
  1338             // when g_vml_ is present, then we can be sure excanvas is active, meaning there's not a real canvas.
       
  1339             
       
  1340              return d && d.g_vml_ ? 
       
  1341                 false :
       
  1342                 $('<canvas />')[0].getContext ? 
       
  1343                     true : 
       
  1344                     false;
       
  1345         }
       
  1346 
       
  1347         /**
       
  1348          * Merge new area data into existing area options on a MapData object. Used for rebinding.
       
  1349          * 
       
  1350          * @param  {[MapData]} map_data     The MapData object
       
  1351          * @param  {[object[]]} areas       areas array to merge
       
  1352          */
       
  1353         
       
  1354         function merge_areas(map_data, areas) {
       
  1355             var ar, index,
       
  1356                 map_areas = map_data.options.areas;
       
  1357 
       
  1358             if (areas) {
       
  1359                 $.each(areas, function (i, e) {
       
  1360                     
       
  1361                     // Issue #68 - ignore invalid data in areas array
       
  1362                     
       
  1363                     if (!e || !e.key) { 
       
  1364                         return;
       
  1365                     }
       
  1366 
       
  1367                     index = u.indexOfProp(map_areas, "key", e.key);
       
  1368 
       
  1369                     if (index >= 0) {
       
  1370                         $.extend(map_areas[index], e);
       
  1371                     }
       
  1372                     else {
       
  1373                         map_areas.push(e);
       
  1374                     }
       
  1375                     ar = map_data.getDataForKey(e.key);
       
  1376                     if (ar) {
       
  1377                         $.extend(ar.options, e);
       
  1378                     }
       
  1379                 });
       
  1380             }
       
  1381         }
       
  1382         function merge_options(map_data, options) {
       
  1383             var temp_opts = u.updateProps({}, options);
       
  1384             delete temp_opts.areas;
       
  1385 
       
  1386             u.updateProps(map_data.options, temp_opts);
       
  1387 
       
  1388             merge_areas(map_data, options.areas);
       
  1389             // refresh the area_option template
       
  1390             u.updateProps(map_data.area_options, map_data.options);
       
  1391         }
       
  1392 
       
  1393         // Most methods use the "Method" object which handles figuring out whether it's an image or area called and
       
  1394         // parsing key parameters. The constructor wants:
       
  1395         // this, the jQuery object
       
  1396         // a function that is called when an image was passed (with a this context of the MapData)
       
  1397         // a function that is called when an area was passed (with a this context of the AreaData)
       
  1398         // options: first = true means only the first member of a jQuery object is handled
       
  1399         //          key = the key parameters passed
       
  1400         //          defaultReturn: a value to return other than the jQuery object (if its not chainable)
       
  1401         //          args: the arguments
       
  1402         // Returns a comma-separated list of user-selected areas. "staticState" areas are not considered selected for the purposes of this method.
       
  1403         
       
  1404         me.get = function (key) {
       
  1405             var md = m.getMapData(this);
       
  1406             if (!(md && md.complete)) {
       
  1407                 throw("Can't access data until binding complete.");
       
  1408             }
       
  1409 
       
  1410             return (new m.Method(this,
       
  1411                 function () {
       
  1412                     // map_data return
       
  1413                     return this.getSelected();
       
  1414                 },
       
  1415                 function () {
       
  1416                     return this.isSelected();
       
  1417                 },
       
  1418                 { name: 'get',
       
  1419                     args: arguments,
       
  1420                     key: key,
       
  1421                     first: true,
       
  1422                     allowAsync: true,
       
  1423                     defaultReturn: ''
       
  1424                 }
       
  1425             )).go();
       
  1426         };
       
  1427         me.data = function (key) {
       
  1428             return (new m.Method(this,
       
  1429                 null,
       
  1430                 function () {
       
  1431                     return this;
       
  1432                 },
       
  1433                 { name: 'data',
       
  1434                     args: arguments,
       
  1435                     key: key
       
  1436                 }
       
  1437             )).go();
       
  1438         };
       
  1439 
       
  1440 
       
  1441         // Set or return highlight state.
       
  1442         //  $(img).mapster('highlight') -- return highlighted area key, or null if none
       
  1443         //  $(area).mapster('highlight') -- highlight an area
       
  1444         //  $(img).mapster('highlight','area_key') -- highlight an area
       
  1445         //  $(img).mapster('highlight',false) -- remove highlight
       
  1446         me.highlight = function (key) {
       
  1447             return (new m.Method(this,
       
  1448                 function () {
       
  1449                     if (key === false) {
       
  1450                         this.ensureNoHighlight();
       
  1451                     } else {
       
  1452                         var id = this.highlightId;
       
  1453                         return id >= 0 ? this.data[id].key : null;
       
  1454                     }
       
  1455                 },
       
  1456                 function () {
       
  1457                     this.highlight();
       
  1458                 },
       
  1459                 { name: 'highlight',
       
  1460                     args: arguments,
       
  1461                     key: key,
       
  1462                     first: true
       
  1463                 }
       
  1464             )).go();
       
  1465         };
       
  1466         // Return the primary keys for an area or group key.
       
  1467         // $(area).mapster('key')
       
  1468         // includes all keys (not just primary keys)
       
  1469         // $(area).mapster('key',true)
       
  1470         // $(img).mapster('key','group-key')
       
  1471 
       
  1472         // $(img).mapster('key','group-key', true)
       
  1473         me.keys = function(key,all) {
       
  1474             var keyList=[], 
       
  1475                 md = m.getMapData(this);
       
  1476 
       
  1477             if (!(md && md.complete)) {
       
  1478                 throw("Can't access data until binding complete.");
       
  1479             }
       
  1480 
       
  1481 
       
  1482             function addUniqueKeys(ad) {
       
  1483                 var areas,keys=[];
       
  1484                 if (!all) {
       
  1485                     keys.push(ad.key);
       
  1486                 } else {
       
  1487                     areas=ad.areas();
       
  1488                     $.each(areas,function(i,e) {
       
  1489                         keys=keys.concat(e.keys);
       
  1490                     });
       
  1491                 }
       
  1492                 $.each(keys,function(i,e) {
       
  1493                     if ($.inArray(e,keyList)<0) {
       
  1494                         keyList.push(e);                         
       
  1495                     }
       
  1496                 });
       
  1497             }
       
  1498 
       
  1499             if (!(md  && md.complete)) {
       
  1500                 return '';
       
  1501             }
       
  1502             if (typeof key === 'string') {
       
  1503                 if (all) {
       
  1504                     addUniqueKeys(md.getDataForKey(key));
       
  1505                 } else {
       
  1506                     keyList=[md.getKeysForGroup(key)];
       
  1507                 }
       
  1508             } else {
       
  1509                 all = key;
       
  1510                 this.each(function(i,e) {
       
  1511                     if (e.nodeName==='AREA') {
       
  1512                         addUniqueKeys(md.getDataForArea(e));
       
  1513                     }
       
  1514                 });
       
  1515             }
       
  1516             return keyList.join(',');
       
  1517         
       
  1518 
       
  1519         };
       
  1520         me.select = function () {
       
  1521             me.set.call(this, true);
       
  1522         };
       
  1523         me.deselect = function () {
       
  1524             me.set.call(this, false);
       
  1525         };
       
  1526         
       
  1527         /**
       
  1528          * Select or unselect areas. Areas can be identified by a single string key, a comma-separated list of keys, 
       
  1529          * or an array of strings.
       
  1530          * 
       
  1531          * 
       
  1532          * @param {boolean} selected Determines whether areas are selected or deselected
       
  1533          * @param {string|string[]} key A string, comma-separated string, or array of strings indicating 
       
  1534          *                              the areas to select or deselect
       
  1535          * @param {object} options Rendering options to apply when selecting an area
       
  1536          */ 
       
  1537 
       
  1538         me.set = function (selected, key, options) {
       
  1539             var lastMap, map_data, opts=options,
       
  1540                 key_list, area_list; // array of unique areas passed
       
  1541 
       
  1542             function setSelection(ar) {
       
  1543                 if (ar) {
       
  1544                     switch (selected) {
       
  1545                         case true:
       
  1546                             ar.select(opts); break;
       
  1547                         case false:
       
  1548                             ar.deselect(true); break;
       
  1549                         default:
       
  1550                             ar.toggle(opts); break;
       
  1551                     }
       
  1552                 }
       
  1553             }
       
  1554             function addArea(ar) {
       
  1555                if (ar && $.inArray(ar, area_list) < 0) {
       
  1556                     area_list.push(ar);
       
  1557                     key_list+=(key_list===''?'':',')+ar.key;
       
  1558                 }
       
  1559             }
       
  1560             // Clean up after a group that applied to the same map
       
  1561             function finishSetForMap(map_data) {
       
  1562                 $.each(area_list, function (i, el) {
       
  1563                     setSelection(el);
       
  1564                 });
       
  1565                 if (!selected) {
       
  1566                     map_data.removeSelectionFinish();
       
  1567                 }
       
  1568                 if (map_data.options.boundList) {
       
  1569                     m.setBoundListProperties(map_data.options, m.getBoundList(map_data.options, key_list), selected);
       
  1570                 }            
       
  1571             }
       
  1572 
       
  1573             this.filter('img,area').each(function (i,e) {
       
  1574                 var keys;
       
  1575                 map_data = m.getMapData(e);
       
  1576 
       
  1577                 if (map_data !== lastMap) {
       
  1578                     if (lastMap) {
       
  1579                        finishSetForMap(lastMap);
       
  1580                     }
       
  1581 
       
  1582                     area_list = [];
       
  1583                     key_list='';
       
  1584                 }
       
  1585                 
       
  1586                if (map_data) {
       
  1587                     
       
  1588                     keys = '';
       
  1589                     if (e.nodeName.toUpperCase()==='IMG') {
       
  1590                         if (!m.queueCommand(map_data, $(e), 'set', [selected, key, opts])) {
       
  1591                             if (key instanceof Array) {
       
  1592                                 if (key.length) {
       
  1593                                     keys = key.join(",");
       
  1594                                 }
       
  1595                             }
       
  1596                             else {
       
  1597                                 keys = key;
       
  1598                             }
       
  1599 
       
  1600                             if (keys) {
       
  1601                                 $.each(u.split(keys), function (i,key) {
       
  1602                                     addArea(map_data.getDataForKey(key.toString()));
       
  1603                                     lastMap = map_data;
       
  1604                                 });
       
  1605                             }
       
  1606                         }
       
  1607                     } else {
       
  1608                         opts=key;
       
  1609                         if (!m.queueCommand(map_data, $(e), 'set', [selected, opts])) {
       
  1610                             addArea(map_data.getDataForArea(e));
       
  1611                             lastMap = map_data;
       
  1612                         }
       
  1613                     
       
  1614                     }
       
  1615                 }
       
  1616             });
       
  1617             
       
  1618             if (map_data) {
       
  1619                finishSetForMap(map_data);
       
  1620             }
       
  1621 
       
  1622            
       
  1623             return this;
       
  1624         };
       
  1625         me.unbind = function (preserveState) {
       
  1626             return (new m.Method(this,
       
  1627                 function () {
       
  1628                     this.clearEvents();
       
  1629                     this.clearMapData(preserveState);
       
  1630                     removeMap(this);
       
  1631                 },
       
  1632                 null,
       
  1633                 { name: 'unbind',
       
  1634                     args: arguments
       
  1635                 }
       
  1636             )).go();
       
  1637         };
       
  1638 
       
  1639 
       
  1640         // refresh options and update selection information.
       
  1641         me.rebind = function (options) {
       
  1642             return (new m.Method(this,
       
  1643                 function () {
       
  1644                     var me=this;
       
  1645 
       
  1646                     me.complete=false;
       
  1647                     me.configureOptions(options);
       
  1648                     me.bindImages().then(function() {
       
  1649                         me.buildDataset(true);
       
  1650                         me.complete=true;
       
  1651                     });
       
  1652                     //this.redrawSelections();
       
  1653                 },
       
  1654                 null,
       
  1655                 {
       
  1656                     name: 'rebind',
       
  1657                     args: arguments
       
  1658                 }
       
  1659             )).go();
       
  1660         };
       
  1661         // get options. nothing or false to get, or "true" to get effective options (versus passed options)
       
  1662         me.get_options = function (key, effective) {
       
  1663             var eff = u.isBool(key) ? key : effective; // allow 2nd parm as "effective" when no key
       
  1664             return (new m.Method(this,
       
  1665                 function () {
       
  1666                     var opts = $.extend({}, this.options);
       
  1667                     if (eff) {
       
  1668                         opts.render_select = u.updateProps(
       
  1669                             {},
       
  1670                             m.render_defaults,
       
  1671                             opts,
       
  1672                             opts.render_select);
       
  1673 
       
  1674                         opts.render_highlight = u.updateProps(
       
  1675                             {},
       
  1676                             m.render_defaults,
       
  1677                             opts,
       
  1678                             opts.render_highlight);
       
  1679                     }
       
  1680                     return opts;
       
  1681                 },
       
  1682                 function () {
       
  1683                     return eff ? this.effectiveOptions() : this.options;
       
  1684                 },
       
  1685                 {
       
  1686                     name: 'get_options',
       
  1687                     args: arguments,
       
  1688                     first: true,
       
  1689                     allowAsync: true,
       
  1690                     key: key
       
  1691                 }
       
  1692             )).go();
       
  1693         };
       
  1694 
       
  1695         // set options - pass an object with options to set,
       
  1696         me.set_options = function (options) {
       
  1697             return (new m.Method(this,
       
  1698                 function () {
       
  1699                     merge_options(this, options);
       
  1700                 },
       
  1701                 null,
       
  1702                 {
       
  1703                     name: 'set_options',
       
  1704                     args: arguments
       
  1705                 }
       
  1706             )).go();
       
  1707         };
       
  1708         me.unload = function () {
       
  1709             var i;
       
  1710             for (i = m.map_cache.length - 1; i >= 0; i--) {
       
  1711                 if (m.map_cache[i]) {
       
  1712                     me.unbind.call($(m.map_cache[i].image));
       
  1713                 }
       
  1714             }
       
  1715             me.graphics = null;
       
  1716         };
       
  1717 
       
  1718         me.snapshot = function () {
       
  1719             return (new m.Method(this,
       
  1720                 function () {
       
  1721                     $.each(this.data, function (i, e) {
       
  1722                         e.selected = false;
       
  1723                     });
       
  1724 
       
  1725                     this.base_canvas = this.graphics.createVisibleCanvas(this);
       
  1726                     $(this.image).before(this.base_canvas);
       
  1727                 },
       
  1728                 null,
       
  1729                 { name: 'snapshot' }
       
  1730             )).go();
       
  1731         };
       
  1732         
       
  1733         // do not queue this function
       
  1734         
       
  1735         me.state = function () {
       
  1736             var md, result = null;
       
  1737             $(this).each(function (i,e) {
       
  1738                 if (e.nodeName === 'IMG') {
       
  1739                     md = m.getMapData(e);
       
  1740                     if (md) {
       
  1741                         result = md.state();
       
  1742                     }
       
  1743                     return false;
       
  1744                 }
       
  1745             });
       
  1746             return result;
       
  1747         };
       
  1748 
       
  1749         me.bind = function (options) {
       
  1750 
       
  1751             return this.each(function (i,e) {
       
  1752                 var img, map, usemap, md;
       
  1753 
       
  1754                 // save ref to this image even if we can't access it yet. commands will be queued
       
  1755                 img = $(e);
       
  1756 
       
  1757                 md = m.getMapData(e);
       
  1758 
       
  1759                 // if already bound completely, do a total rebind
       
  1760                 
       
  1761                 if (md) {
       
  1762                     me.unbind.apply(img);
       
  1763                     if (!md.complete) {
       
  1764                         // will be queued
       
  1765                         img.bind();
       
  1766                         return true;
       
  1767                     }
       
  1768                     md = null;
       
  1769                 }
       
  1770 
       
  1771                 // ensure it's a valid image
       
  1772                 // jQuery bug with Opera, results in full-url#usemap being returned from jQuery's attr.
       
  1773                 // So use raw getAttribute instead.
       
  1774                 
       
  1775                 usemap = this.getAttribute('usemap');
       
  1776                 map = usemap && $('map[name="' + usemap.substr(1) + '"]');
       
  1777                 if (!(img.is('img') && usemap && map.size() > 0)) {
       
  1778                     return true;
       
  1779                 }
       
  1780 
       
  1781                 // sorry - your image must have border:0, things are too unpredictable otherwise.
       
  1782                 img.css('border', 0);
       
  1783 
       
  1784                 if (!md) {
       
  1785                     md = new m.MapData(this, options);
       
  1786 
       
  1787                     md.index = addMap(md);
       
  1788                     md.map = map;
       
  1789                     md.bindImages().then(function() {
       
  1790                         md.initialize();
       
  1791                     });
       
  1792                 }
       
  1793             });
       
  1794         };
       
  1795 
       
  1796         me.init = function (useCanvas) {
       
  1797             var style, shapes;
       
  1798 
       
  1799             // for testing/debugging, use of canvas can be forced by initializing 
       
  1800             // manually with "true" or "false". But generally we test for it.
       
  1801             
       
  1802             m.hasCanvas = function() {
       
  1803                 if (!u.isBool(m.hasCanvas.value)) {
       
  1804                     m.hasCanvas.value = u.isBool(useCanvas) ?
       
  1805                         useCanvas : 
       
  1806                         hasCanvas();
       
  1807                 }
       
  1808                 return m.hasCanvas.value;
       
  1809             };
       
  1810             m.hasVml = function() {
       
  1811                 if (!u.isBool(m.hasVml.value)) {
       
  1812                     // initialize VML the first time we detect its presence.
       
  1813                     var d = namespaces();
       
  1814 
       
  1815                     if (d && !d.v) {
       
  1816                         d.add("v", "urn:schemas-microsoft-com:vml");
       
  1817                         style = document.createStyleSheet();
       
  1818                         shapes = ['shape', 'rect', 'oval', 'circ', 'fill', 'stroke', 'imagedata', 'group', 'textbox'];
       
  1819                         $.each(shapes,
       
  1820                         function (i, el) {
       
  1821                             style.addRule('v\\:' + el, "behavior: url(#default#VML); antialias:true");
       
  1822                         });
       
  1823                     }
       
  1824                     m.hasVml.value = hasVml();
       
  1825                 }
       
  1826 
       
  1827                 return m.hasVml.value;
       
  1828             };
       
  1829 
       
  1830             m.isTouch = !!document.documentElement.ontouchstart;
       
  1831 
       
  1832             $.extend(m.defaults, m.render_defaults,m.shared_defaults);
       
  1833             $.extend(m.area_defaults, m.render_defaults,m.shared_defaults);
       
  1834             
       
  1835         };
       
  1836         me.test = function (obj) {
       
  1837             return eval(obj);
       
  1838         };
       
  1839         return me;
       
  1840     } ());
       
  1841     
       
  1842     $.mapster.impl.init();
       
  1843     
       
  1844     
       
  1845 } (jQuery));
       
  1846 /* graphics.js
       
  1847    Graphics object handles all rendering.
       
  1848 */
       
  1849 (function ($) {
       
  1850     var p, m=$.mapster,
       
  1851         u=m.utils,
       
  1852         canvasMethods,
       
  1853         vmlMethods;
       
  1854     
       
  1855     /**
       
  1856      * Implemenation to add each area in an AreaData object to the canvas
       
  1857      * @param {Graphics} graphics The target graphics object
       
  1858      * @param {AreaData} areaData The AreaData object (a collection of area elements and metadata)
       
  1859      * @param {object} options Rendering options to apply when rendering this group of areas
       
  1860      */
       
  1861     function addShapeGroupImpl(graphics, areaData, options) {
       
  1862         var me = graphics,
       
  1863             md = me.map_data,
       
  1864             isMask = options.isMask;
       
  1865 
       
  1866         // first get area options. Then override fade for selecting, and finally merge in the 
       
  1867         // "select" effect options.
       
  1868 
       
  1869         $.each(areaData.areas(), function (i,e) {
       
  1870             options.isMask = isMask || (e.nohref && md.options.noHrefIsMask);
       
  1871             me.addShape(e, options);
       
  1872         });
       
  1873 
       
  1874         // it's faster just to manipulate the passed options isMask property and restore it, than to 
       
  1875         // copy the object each time
       
  1876         
       
  1877         options.isMask=isMask;
       
  1878 
       
  1879     }
       
  1880 
       
  1881      /**
       
  1882      * Convert a hex value to decimal
       
  1883      * @param  {string} hex A hexadecimal toString
       
  1884      * @return {int} Integer represenation of the hex string
       
  1885      */
       
  1886 
       
  1887     function hex_to_decimal(hex) {
       
  1888         return Math.max(0, Math.min(parseInt(hex, 16), 255));
       
  1889     }
       
  1890     function css3color(color, opacity) {
       
  1891         return 'rgba(' + hex_to_decimal(color.substr(0, 2)) + ','
       
  1892                 + hex_to_decimal(color.substr(2, 2)) + ','
       
  1893                 + hex_to_decimal(color.substr(4, 2)) + ',' + opacity + ')';
       
  1894     }
       
  1895     /**
       
  1896      * An object associated with a particular map_data instance to manage renderin.
       
  1897      * @param {MapData} map_data The MapData object bound to this instance
       
  1898      */
       
  1899     
       
  1900     m.Graphics = function (map_data) {
       
  1901         //$(window).unload($.mapster.unload);
       
  1902         // create graphics functions for canvas and vml browsers. usage:
       
  1903         // 1) init with map_data, 2) call begin with canvas to be used (these are separate b/c may not require canvas to be specified
       
  1904         // 3) call add_shape_to for each shape or mask, 4) call render() to finish
       
  1905 
       
  1906         var me = this;
       
  1907         me.active = false;
       
  1908         me.canvas = null;
       
  1909         me.width = 0;
       
  1910         me.height = 0;
       
  1911         me.shapes = [];
       
  1912         me.masks = [];
       
  1913         me.map_data = map_data;
       
  1914     };
       
  1915     
       
  1916     p = m.Graphics.prototype= {
       
  1917         constructor: m.Graphics,
       
  1918 
       
  1919         /**
       
  1920          * Initiate a graphics request for a canvas
       
  1921          * @param  {Element} canvas The canvas element that is the target of this operation
       
  1922          * @param  {string} [elementName] The name to assign to the element (VML only)
       
  1923          */
       
  1924         
       
  1925         begin: function(canvas, elementName) {
       
  1926             var c = $(canvas);
       
  1927 
       
  1928             this.elementName = elementName;
       
  1929             this.canvas = canvas;
       
  1930 
       
  1931             this.width = c.width();
       
  1932             this.height = c.height();
       
  1933             this.shapes = [];
       
  1934             this.masks = [];
       
  1935             this.active = true;
       
  1936 
       
  1937         },
       
  1938         
       
  1939         /**
       
  1940          * Add an area to be rendered to this canvas. 
       
  1941          * @param {MapArea} mapArea The MapArea object to render
       
  1942          * @param {object} options An object containing any rendering options that should override the
       
  1943          *                         defaults for the area
       
  1944          */
       
  1945         
       
  1946         addShape: function(mapArea, options) {
       
  1947             var addto = options.isMask ? this.masks : this.shapes;
       
  1948             addto.push({ mapArea: mapArea, options: options });
       
  1949         },
       
  1950 
       
  1951         /**
       
  1952          * Create a canvas that is sized and styled for the MapData object
       
  1953          * @param  {MapData} mapData The MapData object that will receive this new canvas
       
  1954          * @return {Element} A canvas element
       
  1955          */
       
  1956         
       
  1957         createVisibleCanvas: function (mapData) {
       
  1958             return $(this.createCanvasFor(mapData))
       
  1959                 .addClass('mapster_el')
       
  1960                 .css(m.canvas_style)[0];
       
  1961         },
       
  1962 
       
  1963         /**
       
  1964          * Add a group of shapes from an AreaData object to the canvas
       
  1965          * 
       
  1966          * @param {AreaData} areaData An AreaData object (a set of area elements)
       
  1967          * @param {string} mode     The rendering mode, "select" or "highlight". This determines the target 
       
  1968          *                          canvas and which default options to use.
       
  1969          * @param {striong} options  Rendering options
       
  1970          */
       
  1971         
       
  1972         addShapeGroup: function (areaData, mode,options) {
       
  1973             // render includeKeys first - because they could be masks
       
  1974             var me = this,
       
  1975                 list, name, canvas,
       
  1976                 map_data = this.map_data,
       
  1977                 opts = areaData.effectiveRenderOptions(mode);
       
  1978 
       
  1979             if (options) {
       
  1980                  $.extend(opts,options);
       
  1981             }
       
  1982 
       
  1983             if (mode === 'select') {
       
  1984                 name = "static_" + areaData.areaId.toString();
       
  1985                 canvas = map_data.base_canvas;
       
  1986             } else {
       
  1987                 canvas = map_data.overlay_canvas;
       
  1988             }
       
  1989 
       
  1990             me.begin(canvas, name);
       
  1991 
       
  1992             if (opts.includeKeys) {
       
  1993                 list = u.split(opts.includeKeys);
       
  1994                 $.each(list, function (i,e) {
       
  1995                     var areaData = map_data.getDataForKey(e.toString());
       
  1996                     addShapeGroupImpl(me,areaData, areaData.effectiveRenderOptions(mode));
       
  1997                 });
       
  1998             }
       
  1999 
       
  2000             addShapeGroupImpl(me,areaData, opts);
       
  2001             me.render();
       
  2002             if (opts.fade) {
       
  2003                 
       
  2004                 // fading requires special handling for IE. We must access the fill elements directly. The fader also has to deal with 
       
  2005                 // the "opacity" attribute (not css)
       
  2006 
       
  2007                 u.fader(m.hasCanvas() ? 
       
  2008                     canvas : 
       
  2009                     $(canvas).find('._fill').not('.mapster_mask'),
       
  2010                 0,
       
  2011                 m.hasCanvas() ? 
       
  2012                     1 : 
       
  2013                     opts.fillOpacity,
       
  2014                 opts.fadeDuration); 
       
  2015                
       
  2016             }
       
  2017 
       
  2018         }
       
  2019 
       
  2020         // These prototype methods are implementation dependent
       
  2021     };
       
  2022 
       
  2023     function noop() {}
       
  2024 
       
  2025   
       
  2026     // configure remaining prototype methods for ie or canvas-supporting browser
       
  2027 
       
  2028     canvasMethods = {
       
  2029         renderShape: function (context, mapArea, offset) {
       
  2030             var i,
       
  2031                 c = mapArea.coords(null,offset);
       
  2032 
       
  2033             switch (mapArea.shape) {
       
  2034                 case 'rect':
       
  2035                     context.rect(c[0], c[1], c[2] - c[0], c[3] - c[1]);
       
  2036                     break;
       
  2037                 case 'poly':
       
  2038                     context.moveTo(c[0], c[1]);
       
  2039 
       
  2040                     for (i = 2; i < mapArea.length; i += 2) {
       
  2041                         context.lineTo(c[i], c[i + 1]);
       
  2042                     }
       
  2043                     context.lineTo(c[0], c[1]);
       
  2044                     break;
       
  2045                 case 'circ':
       
  2046                 case 'circle':
       
  2047                     context.arc(c[0], c[1], c[2], 0, Math.PI * 2, false);
       
  2048                     break;
       
  2049             }
       
  2050         },
       
  2051         addAltImage: function (context, image, mapArea, options) {
       
  2052             context.beginPath();
       
  2053 
       
  2054             this.renderShape(context, mapArea);
       
  2055             context.closePath();
       
  2056             context.clip();
       
  2057 
       
  2058             context.globalAlpha = options.altImageOpacity || options.fillOpacity;
       
  2059 
       
  2060             context.drawImage(image, 0, 0, mapArea.owner.scaleInfo.width, mapArea.owner.scaleInfo.height);
       
  2061         },
       
  2062         render: function () {
       
  2063             // firefox 6.0 context.save() seems to be broken. to work around,  we have to draw the contents on one temp canvas,
       
  2064             // the mask on another, and merge everything. ugh. fixed in 1.2.2. unfortunately this is a lot more code for masks,
       
  2065             // but no other way around it that i can see.
       
  2066 
       
  2067             var maskCanvas, maskContext,
       
  2068                         me = this,
       
  2069                         md = me.map_data,
       
  2070                         hasMasks = me.masks.length,
       
  2071                         shapeCanvas = me.createCanvasFor(md),
       
  2072                         shapeContext = shapeCanvas.getContext('2d'),
       
  2073                         context = me.canvas.getContext('2d');
       
  2074 
       
  2075             if (hasMasks) {
       
  2076                 maskCanvas = me.createCanvasFor(md);
       
  2077                 maskContext = maskCanvas.getContext('2d');
       
  2078                 maskContext.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
       
  2079 
       
  2080                 $.each(me.masks, function (i,e) {
       
  2081                     maskContext.save();
       
  2082                     maskContext.beginPath();
       
  2083                     me.renderShape(maskContext, e.mapArea);
       
  2084                     maskContext.closePath();
       
  2085                     maskContext.clip();
       
  2086                     maskContext.lineWidth = 0;
       
  2087                     maskContext.fillStyle = '#000';
       
  2088                     maskContext.fill();
       
  2089                     maskContext.restore();
       
  2090                 });
       
  2091 
       
  2092             }
       
  2093 
       
  2094             $.each(me.shapes, function (i,s) {
       
  2095                 shapeContext.save();
       
  2096                 if (s.options.fill) {
       
  2097                     if (s.options.altImageId) {
       
  2098                         me.addAltImage(shapeContext, md.images[s.options.altImageId], s.mapArea, s.options);
       
  2099                     } else {
       
  2100                         shapeContext.beginPath();
       
  2101                         me.renderShape(shapeContext, s.mapArea);
       
  2102                         shapeContext.closePath();
       
  2103                         //shapeContext.clip();
       
  2104                         shapeContext.fillStyle = css3color(s.options.fillColor, s.options.fillOpacity);
       
  2105                         shapeContext.fill();
       
  2106                     }
       
  2107                 }
       
  2108                 shapeContext.restore();
       
  2109             });
       
  2110 
       
  2111 
       
  2112             // render strokes at end since masks get stroked too
       
  2113 
       
  2114             $.each(me.shapes.concat(me.masks), function (i,s) {
       
  2115                 var offset = s.options.strokeWidth === 1 ? 0.5 : 0;
       
  2116                 // offset applies only when stroke width is 1 and stroke would render between pixels.
       
  2117 
       
  2118                 if (s.options.stroke) {
       
  2119                     shapeContext.save();
       
  2120                     shapeContext.strokeStyle = css3color(s.options.strokeColor, s.options.strokeOpacity);
       
  2121                     shapeContext.lineWidth = s.options.strokeWidth;
       
  2122 
       
  2123                     shapeContext.beginPath();
       
  2124 
       
  2125                     me.renderShape(shapeContext, s.mapArea, offset);
       
  2126                     shapeContext.closePath();
       
  2127                     shapeContext.stroke();
       
  2128                     shapeContext.restore();
       
  2129                 }
       
  2130             });
       
  2131 
       
  2132             if (hasMasks) {
       
  2133                 // render the new shapes against the mask
       
  2134 
       
  2135                 maskContext.globalCompositeOperation = "source-out";
       
  2136                 maskContext.drawImage(shapeCanvas, 0, 0);
       
  2137 
       
  2138                 // flatten into the main canvas
       
  2139                 context.drawImage(maskCanvas, 0, 0);
       
  2140             } else {
       
  2141                 context.drawImage(shapeCanvas, 0, 0);
       
  2142             }
       
  2143 
       
  2144             me.active = false;
       
  2145             return me.canvas;
       
  2146         },
       
  2147 
       
  2148         // create a canvas mimicing dimensions of an existing element
       
  2149         createCanvasFor: function (md) {
       
  2150             return $('<canvas width="' + md.scaleInfo.width + '" height="' +md.scaleInfo.height + '"></canvas>')[0];
       
  2151         },
       
  2152         clearHighlight: function () {
       
  2153             var c = this.map_data.overlay_canvas;
       
  2154             c.getContext('2d').clearRect(0, 0, c.width, c.height);
       
  2155         },
       
  2156         // Draw all items from selected_list to a new canvas, then swap with the old one. This is used to delete items when using canvases.
       
  2157         refreshSelections: function () {
       
  2158             var canvas_temp, map_data = this.map_data;
       
  2159             // draw new base canvas, then swap with the old one to avoid flickering
       
  2160             canvas_temp = map_data.base_canvas;
       
  2161 
       
  2162             map_data.base_canvas = this.createVisibleCanvas(map_data);
       
  2163             $(map_data.base_canvas).hide();
       
  2164             $(canvas_temp).before(map_data.base_canvas);
       
  2165 
       
  2166             map_data.redrawSelections();
       
  2167 
       
  2168             $(map_data.base_canvas).show();
       
  2169             $(canvas_temp).remove();
       
  2170         }
       
  2171     };
       
  2172 
       
  2173     vmlMethods = {
       
  2174 
       
  2175         renderShape: function (mapArea, options, cssclass) {
       
  2176             var me = this, fill,stroke, e, t_fill, el_name, el_class, template, c = mapArea.coords();
       
  2177             el_name = me.elementName ? 'name="' + me.elementName + '" ' : '';
       
  2178             el_class = cssclass ? 'class="' + cssclass + '" ' : '';
       
  2179 
       
  2180             t_fill = '<v:fill color="#' + options.fillColor + '" class="_fill" opacity="' + 
       
  2181                 (options.fill ? 
       
  2182                     options.fillOpacity :
       
  2183                     0) + 
       
  2184                 '" /><v:stroke class="_fill" opacity="' + 
       
  2185                 options.strokeOpacity + '"/>';
       
  2186 
       
  2187 
       
  2188             stroke = options.stroke ?
       
  2189                 ' strokeweight=' + options.strokeWidth + ' stroked="t" strokecolor="#' + 
       
  2190                     options.strokeColor + '"' :
       
  2191                 ' stroked="f"';
       
  2192             
       
  2193             fill = options.fill ? 
       
  2194                 ' filled="t"' :
       
  2195                 ' filled="f"';
       
  2196 
       
  2197             switch (mapArea.shape) {
       
  2198                 case 'rect':
       
  2199                     template = '<v:rect ' + el_class + el_name + fill + stroke + 
       
  2200                         ' style="zoom:1;margin:0;padding:0;display:block;position:absolute;left:' + 
       
  2201                           c[0] + 'px;top:' + c[1]  + 'px;width:' + (c[2] - c[0]) + 
       
  2202                           'px;height:' + (c[3] - c[1]) + 'px;">' + t_fill + '</v:rect>';
       
  2203                     break;
       
  2204                 case 'poly':
       
  2205                     template = '<v:shape ' + el_class + el_name + fill + stroke + ' coordorigin="0,0" coordsize="' + me.width + ',' + me.height
       
  2206                                 + '" path="m ' + c[0] + ',' + c[1] + ' l ' + c.slice(2).join(',')
       
  2207                                 + ' x e" style="zoom:1;margin:0;padding:0;display:block;position:absolute;top:0px;left:0px;width:' + me.width + 'px;height:' + me.height + 'px;">' + t_fill + '</v:shape>';
       
  2208                     break;
       
  2209                 case 'circ':
       
  2210                 case 'circle':
       
  2211                     template = '<v:oval ' + el_class + el_name + fill + stroke
       
  2212                                 + ' style="zoom:1;margin:0;padding:0;display:block;position:absolute;left:' + (c[0] - c[2]) + 'px;top:' + (c[1] - c[2])
       
  2213                                 + 'px;width:' + (c[2] * 2) + 'px;height:' + (c[2] * 2) + 'px;">' + t_fill + '</v:oval>';
       
  2214                     break;
       
  2215             }
       
  2216             e = $(template);
       
  2217             $(me.canvas).append(e);
       
  2218 
       
  2219             return e;
       
  2220         },
       
  2221         render: function () {
       
  2222             var opts, me = this;
       
  2223 
       
  2224             $.each(this.shapes, function (i,e) {
       
  2225                 me.renderShape(e.mapArea, e.options);
       
  2226             });
       
  2227 
       
  2228             if (this.masks.length) {
       
  2229                 $.each(this.masks, function (i,e) {
       
  2230                     opts = u.updateProps({},
       
  2231                         e.options, {
       
  2232                             fillOpacity: 1,
       
  2233                             fillColor: e.options.fillColorMask
       
  2234                         });
       
  2235                     me.renderShape(e.mapArea, opts, 'mapster_mask');
       
  2236                 });
       
  2237             }
       
  2238 
       
  2239             this.active = false;
       
  2240             return this.canvas;
       
  2241         },
       
  2242 
       
  2243         createCanvasFor: function (md) {
       
  2244             var w = md.scaleInfo.width,
       
  2245                 h = md.scaleInfo.height;
       
  2246             return $('<var width="' + w + '" height="' + h 
       
  2247                 + '" style="zoom:1;overflow:hidden;display:block;width:' 
       
  2248                 + w + 'px;height:' + h + 'px;"></var>')[0];
       
  2249         },
       
  2250 
       
  2251         clearHighlight: function () {
       
  2252             $(this.map_data.overlay_canvas).children().remove();
       
  2253         },
       
  2254         // remove single or all selections
       
  2255         removeSelections: function (area_id) {
       
  2256             if (area_id >= 0) {
       
  2257                 $(this.map_data.base_canvas).find('[name="static_' + area_id.toString() + '"]').remove();
       
  2258             }
       
  2259             else {
       
  2260                 $(this.map_data.base_canvas).children().remove();
       
  2261             }
       
  2262         }
       
  2263 
       
  2264     };
       
  2265 
       
  2266     // for all methods with two implemenatations, add a function that will automatically replace itself with the correct
       
  2267     // method on first invocation
       
  2268     
       
  2269     $.each(['renderShape',
       
  2270            'addAltImage',
       
  2271            'render',
       
  2272            'createCanvasFor',
       
  2273            'clearHighlight',
       
  2274            'removeSelections',
       
  2275            'refreshSelections'],
       
  2276         function(i,e) {
       
  2277             p[e]=(function(method) {
       
  2278                 return function() {
       
  2279                     p[method] = (m.hasCanvas() ?
       
  2280                         canvasMethods[method] : 
       
  2281                         vmlMethods[method]) || noop;
       
  2282                   
       
  2283                     return p[method].apply(this,arguments);
       
  2284                 };
       
  2285             }(e));
       
  2286     });
       
  2287 
       
  2288 
       
  2289 } (jQuery));
       
  2290 /* mapimage.js
       
  2291    the MapImage object, repesents an instance of a single bound imagemap
       
  2292 */
       
  2293 
       
  2294 (function ($) {
       
  2295 
       
  2296     var m = $.mapster, 
       
  2297         u = m.utils,
       
  2298         ap=[];
       
  2299     /**
       
  2300      * An object encapsulating all the images used by a MapData.
       
  2301      */
       
  2302     
       
  2303     m.MapImages = function(owner) {
       
  2304         this.owner = owner;
       
  2305         this.clear();
       
  2306     };
       
  2307 
       
  2308     
       
  2309     m.MapImages.prototype = {
       
  2310         constructor: m.MapImages,
       
  2311 
       
  2312         /* interface to make this array-like */
       
  2313 
       
  2314         slice: function() {
       
  2315             return ap.slice.apply(this,arguments);
       
  2316         },
       
  2317         splice: function() {
       
  2318             ap.slice.apply(this.status,arguments);
       
  2319             var result= ap.slice.apply(this,arguments);
       
  2320             return result;
       
  2321         },
       
  2322      
       
  2323         /** 
       
  2324          * a boolean value indicates whether all images are done loading 
       
  2325          * @return {bool} true when all are done
       
  2326          */
       
  2327         complete: function() {
       
  2328             return $.inArray(false, this.status) < 0;
       
  2329         },
       
  2330         
       
  2331         /**
       
  2332          * Save an image in the images array and return its index 
       
  2333          * @param  {Image} image An Image object
       
  2334          * @return {int} the index of the image
       
  2335          */
       
  2336         
       
  2337         _add: function(image) {
       
  2338             var index = ap.push.call(this,image)-1;
       
  2339             this.status[index] = false;
       
  2340             return index;
       
  2341         },
       
  2342 
       
  2343         /**
       
  2344          * Return the index of an Image within the images array
       
  2345          * @param  {Image} img An Image
       
  2346          * @return {int} the index within the array, or -1 if it was not found
       
  2347          */
       
  2348         
       
  2349         indexOf: function(image) {
       
  2350             return $.inArray(image, this);
       
  2351         },
       
  2352         
       
  2353         /**
       
  2354          * Clear this object and reset it to its initial state after binding.
       
  2355          */
       
  2356         
       
  2357         clear: function() {
       
  2358             var me=this;
       
  2359 
       
  2360             if (me.ids && me.ids.length>0) {
       
  2361                 $.each(me.ids,function(i,e) {
       
  2362                     delete me[e];
       
  2363                 });
       
  2364             }
       
  2365             
       
  2366             /**
       
  2367              * A list of the cross-reference IDs bound to this object
       
  2368              * @type {string[]}
       
  2369              */
       
  2370             
       
  2371             me.ids=[];
       
  2372 
       
  2373             /**
       
  2374              * Length property for array-like behavior, set to zero when initializing. Array prototype
       
  2375              * methods will update it after that.
       
  2376              * 
       
  2377              * @type {int}
       
  2378              */
       
  2379             
       
  2380             me.length=0;
       
  2381 
       
  2382             /**
       
  2383              * the loaded status of the corresponding image
       
  2384              * @type {boolean[]}
       
  2385              */
       
  2386             
       
  2387             me.status=[];
       
  2388             
       
  2389             
       
  2390             // actually erase the images
       
  2391             
       
  2392             me.splice(0);
       
  2393             
       
  2394         },
       
  2395 
       
  2396         /**
       
  2397          * Bind an image to the map and add it to the queue to be loaded; return an ID that
       
  2398          * can be used to reference the
       
  2399          * 
       
  2400          * @param {Image|string} image An Image object or a URL to an image
       
  2401          * @param {string} [id] An id to refer to this image
       
  2402          * @returns {int} an ID referencing the index of the image object in 
       
  2403          *                map_data.images
       
  2404          */
       
  2405      
       
  2406         add: function(image,id) {
       
  2407             var index,src,me = this;
       
  2408 
       
  2409             if (!image) { return; }
       
  2410 
       
  2411             if (typeof image === 'string') {
       
  2412                 src = image;
       
  2413                 image = me[src];
       
  2414                 if (typeof image==='object') {
       
  2415                     return me.indexOf(image);
       
  2416                 }
       
  2417 
       
  2418                 image = $('<img />')
       
  2419                     .addClass('mapster_el')
       
  2420                     .hide();
       
  2421 
       
  2422                 index=me._add(image[0]);
       
  2423                 
       
  2424                 image
       
  2425                     .bind('load',function(e) {
       
  2426                         me.imageLoaded.call(me,e);
       
  2427                     })
       
  2428                     .bind('error',function(e) {
       
  2429                         me.imageLoadError.call(me,e);
       
  2430                     });
       
  2431                 
       
  2432                 image.attr('src', src);
       
  2433             } else {
       
  2434 
       
  2435                 // use attr because we want the actual source, not the resolved path the browser will return directly calling image.src
       
  2436                 
       
  2437                 index=me._add($(image)[0]);
       
  2438             }
       
  2439             if (id) {
       
  2440                 if (this[id]) {
       
  2441                     throw(id+" is already used or is not available as an altImage alias.");
       
  2442                 }
       
  2443                 me.ids.push(id);
       
  2444                 me[id]=me[index];
       
  2445             }
       
  2446             return index;
       
  2447         },
       
  2448 
       
  2449         /**
       
  2450          * Bind the images in this object, 
       
  2451          * @param  {boolean} retry when true, indicates that the function is calling itself after failure 
       
  2452          * @return {Promise} a promise that resolves when the images have finished loading
       
  2453          */
       
  2454         
       
  2455         bind: function(retry) {
       
  2456             var me = this,
       
  2457                 promise,
       
  2458                 triesLeft = me.owner.options.configTimeout / 200,
       
  2459 
       
  2460             /* A recursive function to continue checking that the images have been 
       
  2461                loaded until a timeout has elapsed */
       
  2462 
       
  2463             check=function() {
       
  2464                 var i;
       
  2465 
       
  2466                 // refresh status of images
       
  2467                 
       
  2468                 i=me.length;
       
  2469 
       
  2470                 while (i-->0) {
       
  2471                     if (!me.isLoaded(i)) {
       
  2472                         break;
       
  2473                     }
       
  2474                 }
       
  2475 
       
  2476                 // check to see if every image has already been loaded
       
  2477                 
       
  2478                 if (me.complete()) {
       
  2479                     me.resolve();
       
  2480                 } else {
       
  2481                     // to account for failure of onLoad to fire in rare situations
       
  2482                     if (triesLeft-- > 0) {
       
  2483                         me.imgTimeout=window.setTimeout(function() {
       
  2484                             check.call(me,true);
       
  2485                         }, 50);
       
  2486                     } else {
       
  2487                         me.imageLoadError.call(me);
       
  2488                     }
       
  2489                 }
       
  2490             
       
  2491             };
       
  2492 
       
  2493             promise = me.deferred=u.defer();
       
  2494             
       
  2495             check();
       
  2496             return promise;
       
  2497         },
       
  2498    
       
  2499         resolve: function() {
       
  2500             var me=this,
       
  2501                 resolver=me.deferred;
       
  2502                 
       
  2503             if (resolver) {
       
  2504                 // Make a copy of the resolver before calling & removing it to ensure
       
  2505                 // it is not called twice
       
  2506                 me.deferred=null;
       
  2507                 resolver.resolve();
       
  2508             }
       
  2509         },
       
  2510 
       
  2511         /**
       
  2512          * Event handler for image onload
       
  2513          * @param  {object} e jQuery event data
       
  2514          */
       
  2515         
       
  2516         imageLoaded: function(e) {
       
  2517             var me=this,
       
  2518                 index = me.indexOf(e.target);
       
  2519 
       
  2520             if (index>=0) {
       
  2521 
       
  2522                 me.status[index] = true;
       
  2523                 if ($.inArray(false, me.status) < 0) {
       
  2524                     me.resolve();
       
  2525                 }
       
  2526             }
       
  2527         },
       
  2528         
       
  2529         /**
       
  2530          * Event handler for onload error
       
  2531          * @param  {object} e jQuery event data
       
  2532          */
       
  2533         
       
  2534         imageLoadError: function(e) {
       
  2535             clearTimeout(this.imgTimeout);
       
  2536             this.triesLeft=0;
       
  2537             var err = e ? 'The image ' + e.target.src + ' failed to load.' : 
       
  2538                 'The images never seemed to finish loading. You may just need to increase the configTimeout if images could take a long time to load.';
       
  2539             throw err;
       
  2540         },
       
  2541         /**
       
  2542          * Test if the image at specificed index has finished loading
       
  2543          * @param  {int}  index The image index
       
  2544          * @return {boolean} true if loaded, false if not
       
  2545          */
       
  2546         
       
  2547         isLoaded: function(index) {
       
  2548             var img,
       
  2549                 me=this,
       
  2550                 status=me.status;
       
  2551 
       
  2552             if (status[index]) { return true; }
       
  2553             img = me[index];
       
  2554             
       
  2555             if (typeof img.complete !== 'undefined') {
       
  2556                 status[index]=img.complete;
       
  2557             } else {
       
  2558                 status[index]=!!u.imgWidth(img);
       
  2559             }
       
  2560             // if complete passes, the image is loaded, but may STILL not be available because of stuff like adblock.
       
  2561             // make sure it is.
       
  2562 
       
  2563             return status[index];
       
  2564         }
       
  2565     };
       
  2566     } (jQuery));
       
  2567 /* mapdata.js
       
  2568    the MapData object, repesents an instance of a single bound imagemap
       
  2569 */
       
  2570 
       
  2571 
       
  2572 (function ($) {
       
  2573 
       
  2574     var m = $.mapster, 
       
  2575         u = m.utils;
       
  2576    
       
  2577     /**
       
  2578      * Set default values for MapData object properties
       
  2579      * @param  {MapData} me The MapData object
       
  2580      */
       
  2581     
       
  2582     function initializeDefaults(me) {
       
  2583         $.extend(me,{
       
  2584             complete: false,         // (bool)    when configuration is complete       
       
  2585             map: null,                // ($)      the image map
       
  2586             base_canvas: null,       // (canvas|var)  where selections are rendered
       
  2587             overlay_canvas: null,    // (canvas|var)  where highlights are rendered
       
  2588             commands: [],            // {}        commands that were run before configuration was completed (b/c images weren't loaded)
       
  2589             data: [],                // MapData[] area groups
       
  2590             mapAreas: [],            // MapArea[] list. AreaData entities contain refs to this array, so options are stored with each.
       
  2591             _xref: {},               // (int)      xref of mapKeys to data[]
       
  2592             highlightId: -1,        // (int)      the currently highlighted element.
       
  2593             currentAreaId: -1,
       
  2594             _tooltip_events: [],     // {}         info on events we bound to a tooltip container, so we can properly unbind them
       
  2595             scaleInfo: null,         // {}         info about the image size, scaling, defaults
       
  2596             index: -1,                 // index of this in map_cache - so we have an ID to use for wraper div
       
  2597             activeAreaEvent: null
       
  2598         });
       
  2599     }
       
  2600 
       
  2601     /**
       
  2602      * Return an array of all image-containing options from an options object; 
       
  2603      * that is, containers that may have an "altImage" property
       
  2604      * 
       
  2605      * @param  {object} obj     An options object
       
  2606      * @return {object[]}       An array of objects
       
  2607      */
       
  2608     function getOptionImages(obj) {
       
  2609         return [obj, obj.render_highlight, obj.render_select];
       
  2610     }
       
  2611 
       
  2612     /**
       
  2613      * Parse all the altImage references, adding them to the library so they can be preloaded
       
  2614      * and aliased.
       
  2615      * 
       
  2616      * @param  {MapData} me The MapData object on which to operate
       
  2617      */
       
  2618     function configureAltImages(me)
       
  2619     {
       
  2620         var opts = me.options,
       
  2621             mi = me.images;
       
  2622 
       
  2623         // add alt images
       
  2624         
       
  2625         if (m.hasCanvas()) {
       
  2626             // map altImage library first
       
  2627             
       
  2628             $.each(opts.altImages || {}, function(i,e) {
       
  2629                 mi.add(e,i);
       
  2630             });
       
  2631             
       
  2632             // now find everything else
       
  2633 
       
  2634             $.each([opts].concat(opts.areas),function(i,e) {
       
  2635                 $.each(getOptionImages(e),function(i2,e2) {
       
  2636                     if (e2 && e2.altImage) {
       
  2637                         e2.altImageId=mi.add(e2.altImage);
       
  2638                     }
       
  2639                 });
       
  2640             });
       
  2641         }
       
  2642 
       
  2643         // set area_options
       
  2644         me.area_options = u.updateProps({}, // default options for any MapArea
       
  2645             m.area_defaults,
       
  2646             opts);
       
  2647     }
       
  2648 
       
  2649     /**
       
  2650      * Queue a mouse move action based on current delay settings 
       
  2651      * (helper for mouseover/mouseout handlers)
       
  2652      * 
       
  2653      * @param  {MapData}    me       The MapData context
       
  2654      * @param  {number}     delay    The number of milliseconds to delay the action
       
  2655      * @param  {AreaData}   area     AreaData affected
       
  2656      * @param  {Deferred}   deferred A deferred object to return (instead of a new one)
       
  2657      * @return {Promise}    A promise that resolves when the action is completed
       
  2658      */
       
  2659     function queueMouseEvent(me,delay,area, deferred) {
       
  2660         
       
  2661         deferred = deferred || u.when.defer();
       
  2662 
       
  2663         function cbFinal(areaId) {
       
  2664             if (me.currentAreaId!==areaId && me.highlightId>=0) {
       
  2665                 deferred.resolve();
       
  2666             }
       
  2667         }
       
  2668         if (me.activeAreaEvent) {
       
  2669             window.clearTimeout(me.activeAreaEvent);
       
  2670             me.activeAreaEvent=0;
       
  2671         }
       
  2672         if (delay<0) {
       
  2673             return;
       
  2674         }
       
  2675 
       
  2676         if (area.owner.currentAction || delay) {
       
  2677             me.activeAreaEvent = window.setTimeout((function() {
       
  2678                     return function() {
       
  2679                         queueMouseEvent(me,0,area,deferred);
       
  2680                     };
       
  2681                 }(area)),
       
  2682                 delay || 100);
       
  2683         } else {
       
  2684              cbFinal(area.areaId);
       
  2685         }
       
  2686         return deferred;
       
  2687     }
       
  2688 
       
  2689      /**
       
  2690      * Mousedown event. This is captured only to prevent browser from drawing an outline around an
       
  2691      * area when it's clicked.
       
  2692      *
       
  2693      * @param  {EventData} e jQuery event data
       
  2694      */
       
  2695     
       
  2696     function mousedown(e) {
       
  2697         if (!m.hasCanvas()) {
       
  2698             this.blur();
       
  2699         }
       
  2700         e.preventDefault();
       
  2701     }
       
  2702 
       
  2703     /**
       
  2704      * Mouseover event. Handle highlight rendering and client callback on mouseover
       
  2705      * 
       
  2706      * @param  {MapData} me The MapData context
       
  2707      * @param  {EventData} e jQuery event data
       
  2708      * @return {[type]}   [description]
       
  2709      */
       
  2710     
       
  2711     function mouseover(me,e) {
       
  2712         var arData = me.getAllDataForArea(this),
       
  2713             ar=arData.length ? arData[0] : null;
       
  2714 
       
  2715         // mouseover events are ignored entirely while resizing, though we do care about mouseout events
       
  2716         // and must queue the action to keep things clean.
       
  2717 
       
  2718         if (!ar || ar.isNotRendered() || ar.owner.currentAction) {
       
  2719             return;
       
  2720         }
       
  2721 
       
  2722         if (me.currentAreaId === ar.areaId) {
       
  2723             return;
       
  2724         }
       
  2725         if (me.highlightId !== ar.areaId) {
       
  2726             me.clearEffects();
       
  2727 
       
  2728             ar.highlight();
       
  2729 
       
  2730             if (me.options.showToolTip) {
       
  2731                 $.each(arData,function(i,e) {
       
  2732                     if (e.effectiveOptions().toolTip) {
       
  2733                         e.showToolTip();
       
  2734                     }
       
  2735                 });
       
  2736             }
       
  2737         }
       
  2738 
       
  2739         me.currentAreaId = ar.areaId;
       
  2740 
       
  2741         if ($.isFunction(me.options.onMouseover)) {
       
  2742             me.options.onMouseover.call(this,
       
  2743             {
       
  2744                 e: e,
       
  2745                 options:ar.effectiveOptions(),
       
  2746                 key: ar.key,
       
  2747                 selected: ar.isSelected()
       
  2748             });
       
  2749         }
       
  2750     }
       
  2751 
       
  2752     /**
       
  2753      * Mouseout event.
       
  2754      *
       
  2755      * @param  {MapData} me The MapData context
       
  2756      * @param  {EventData} e jQuery event data
       
  2757      * @return {[type]}   [description]
       
  2758      */
       
  2759     
       
  2760     function mouseout(me,e) {
       
  2761         var newArea,
       
  2762             ar = me.getDataForArea(this),
       
  2763             opts = me.options;
       
  2764 
       
  2765 
       
  2766         if (me.currentAreaId<0 || !ar) {
       
  2767             return;
       
  2768         }
       
  2769 
       
  2770         newArea=me.getDataForArea(e.relatedTarget);
       
  2771         
       
  2772         if (newArea === ar) {
       
  2773             return;
       
  2774         }
       
  2775 
       
  2776         me.currentAreaId = -1;
       
  2777         ar.area=null;
       
  2778 
       
  2779         queueMouseEvent(me,opts.mouseoutDelay,ar)
       
  2780             .then(me.clearEffects);
       
  2781 
       
  2782         if ($.isFunction(opts.onMouseout)) {
       
  2783             opts.onMouseout.call(this,
       
  2784             {
       
  2785                 e: e,
       
  2786                 options: opts,
       
  2787                 key: ar.key,
       
  2788                 selected: ar.isSelected()
       
  2789             });
       
  2790         }
       
  2791 
       
  2792     }
       
  2793     
       
  2794     /**
       
  2795      * Clear any active tooltip or highlight
       
  2796      *
       
  2797      * @param  {MapData} me The MapData context
       
  2798      * @param  {EventData} e jQuery event data
       
  2799      * @return {[type]}   [description]
       
  2800      */
       
  2801     
       
  2802     function clearEffects(me) {
       
  2803         var opts = me.options;
       
  2804 
       
  2805         me.ensureNoHighlight();
       
  2806 
       
  2807         if (opts.toolTipClose 
       
  2808             && $.inArray('area-mouseout', opts.toolTipClose) >= 0 
       
  2809             && me.activeToolTip) 
       
  2810         {
       
  2811             me.clearToolTip();
       
  2812         }
       
  2813     }
       
  2814 
       
  2815     /**
       
  2816      * Mouse click event handler
       
  2817      *
       
  2818      * @param  {MapData} me The MapData context
       
  2819      * @param  {EventData} e jQuery event data
       
  2820      * @return {[type]}   [description]
       
  2821      */
       
  2822     
       
  2823     function click(me,e) {
       
  2824         var selected, list, list_target, newSelectionState, canChangeState, cbResult,
       
  2825             that = this,
       
  2826             ar = me.getDataForArea(this),
       
  2827             opts = me.options;
       
  2828 
       
  2829         function clickArea(ar) {
       
  2830             var areaOpts,target;
       
  2831             canChangeState = (ar.isSelectable() &&
       
  2832                 (ar.isDeselectable() || !ar.isSelected()));
       
  2833             
       
  2834             if (canChangeState) {
       
  2835                 newSelectionState = !ar.isSelected();
       
  2836             } else {
       
  2837                 newSelectionState = ar.isSelected();
       
  2838             }
       
  2839 
       
  2840             list_target = m.getBoundList(opts, ar.key);
       
  2841 
       
  2842             if ($.isFunction(opts.onClick)) 
       
  2843             {
       
  2844                 cbResult= opts.onClick.call(that,
       
  2845                 {
       
  2846                     e: e,
       
  2847                     listTarget: list_target,
       
  2848                     key: ar.key,
       
  2849                     selected: newSelectionState
       
  2850                 });
       
  2851 
       
  2852                 if (u.isBool(cbResult)) {
       
  2853                     if (!cbResult) {
       
  2854                         return false;
       
  2855                     }
       
  2856                     target = $(ar.area).attr('href');
       
  2857                     if (target!=='#') {
       
  2858                         window.location.href=target;
       
  2859                         return false;
       
  2860                     }
       
  2861                 }
       
  2862             }
       
  2863 
       
  2864             if (canChangeState) {
       
  2865                 selected = ar.toggle();
       
  2866             }
       
  2867 
       
  2868             if (opts.boundList && opts.boundList.length > 0) {
       
  2869                 m.setBoundListProperties(opts, list_target, ar.isSelected());
       
  2870             }
       
  2871 
       
  2872             areaOpts = ar.effectiveOptions();
       
  2873             if (areaOpts.includeKeys) {
       
  2874                 list = u.split(areaOpts.includeKeys);
       
  2875                 $.each(list, function (i, e) {
       
  2876                     var ar = me.getDataForKey(e.toString());
       
  2877                     if (!ar.options.isMask) {
       
  2878                         clickArea(ar);
       
  2879                     }
       
  2880                 });
       
  2881             }
       
  2882         }
       
  2883 
       
  2884         mousedown.call(this,e);
       
  2885 
       
  2886         if (opts.clickNavigate && ar.href) {
       
  2887             window.location.href=ar.href;
       
  2888             return;
       
  2889         }
       
  2890 
       
  2891         if (ar && !ar.owner.currentAction) {
       
  2892             opts = me.options;
       
  2893             clickArea(ar);
       
  2894         }
       
  2895     }
       
  2896 
       
  2897     /**
       
  2898      * Prototype for a MapData object, representing an ImageMapster bound object
       
  2899      * @param {Element} image   an IMG element
       
  2900      * @param {object} options  ImageMapster binding options
       
  2901      */
       
  2902     m.MapData = function (image, options) 
       
  2903     {
       
  2904         var me = this;
       
  2905         
       
  2906         // (Image)  main map image
       
  2907         
       
  2908         me.image = image;              
       
  2909 
       
  2910         me.images = new m.MapImages(me); 
       
  2911         me.graphics = new m.Graphics(me);
       
  2912 
       
  2913         // save the initial style of the image for unbinding. This is problematic, chrome 
       
  2914         // duplicates styles when assigning, and cssText is apparently not universally supported.
       
  2915         // Need to do something more robust to make unbinding work universally.
       
  2916         
       
  2917         me.imgCssText = image.style.cssText || null;
       
  2918 
       
  2919         initializeDefaults(me);
       
  2920 
       
  2921         me.configureOptions(options);
       
  2922         
       
  2923         // create context-bound event handlers from our private functions
       
  2924         
       
  2925         me.mouseover = function(e) { mouseover.call(this,me,e); };
       
  2926         me.mouseout = function(e) { mouseout.call(this,me,e); };
       
  2927         me.click = function(e) { click.call(this,me,e); };
       
  2928         me.clearEffects = function(e) { clearEffects.call(this,me,e); };
       
  2929     };
       
  2930 
       
  2931     m.MapData.prototype = {
       
  2932         constructor: m.MapData,
       
  2933 
       
  2934          /**
       
  2935          * Set target.options from defaults + options
       
  2936          * @param  {[type]} target      The target
       
  2937          * @param  {[type]} options     The options to merge
       
  2938          */
       
  2939         
       
  2940         configureOptions: function(options) {
       
  2941             this.options= u.updateProps({}, m.defaults, options);
       
  2942         },
       
  2943 
       
  2944         /**
       
  2945          * Ensure all images are loaded
       
  2946          * @return {Promise} A promise that resolves when the images have finished loading (or fail)
       
  2947          */
       
  2948     
       
  2949         bindImages: function() {
       
  2950             var me=this,
       
  2951                 mi = me.images;
       
  2952 
       
  2953             // reset the images if this is a rebind
       
  2954             
       
  2955             if (mi.length>2) {
       
  2956                 mi.splice(2);
       
  2957             } else if (mi.length===0) {
       
  2958 
       
  2959                 // add the actual main image
       
  2960                 mi.add(me.image);
       
  2961                 // will create a duplicate of the main image, we need this to get raw size info
       
  2962                 mi.add(me.image.src);
       
  2963             }
       
  2964             
       
  2965             configureAltImages(me);
       
  2966 
       
  2967             return me.images.bind();
       
  2968         },
       
  2969 
       
  2970         /**
       
  2971          * Test whether an async action is currently in progress
       
  2972          * @return {Boolean} true or false indicating state
       
  2973          */
       
  2974         
       
  2975         isActive: function() {
       
  2976             return !this.complete || this.currentAction;
       
  2977         },
       
  2978 
       
  2979         /**
       
  2980          * Return an object indicating the various states. This isn't really used by 
       
  2981          * production code.
       
  2982          * 
       
  2983          * @return {object} An object with properties for various states
       
  2984          */
       
  2985         
       
  2986         state: function () {
       
  2987             return {
       
  2988                 complete: this.complete,
       
  2989                 resizing: this.currentAction==='resizing',
       
  2990                 zoomed: this.zoomed,
       
  2991                 zoomedArea: this.zoomedArea,
       
  2992                 scaleInfo: this.scaleInfo
       
  2993             };
       
  2994         },   
       
  2995 
       
  2996         /**
       
  2997          * Get a unique ID for the wrapper of this imagemapster
       
  2998          * @return {string} A string that is unique to this image
       
  2999          */
       
  3000         
       
  3001         wrapId: function () {
       
  3002             return 'mapster_wrap_' + this.index;
       
  3003         },
       
  3004         _idFromKey: function (key) {
       
  3005             return typeof key === "string" && this._xref.hasOwnProperty(key) ?
       
  3006                         this._xref[key] : -1;
       
  3007         },
       
  3008 
       
  3009         /**
       
  3010          * Return a comma-separated string of all selected keys
       
  3011          * @return {string} CSV of all keys that are currently selected
       
  3012          */
       
  3013         
       
  3014         getSelected: function () {
       
  3015             var result = '';
       
  3016             $.each(this.data, function (i,e) {
       
  3017                 if (e.isSelected()) {
       
  3018                     result += (result ? ',' : '') + this.key;
       
  3019                 }
       
  3020             });
       
  3021             return result;
       
  3022         },
       
  3023 
       
  3024         /**
       
  3025          * Get an array of MapAreas associated with a specific AREA based on the keys for that area
       
  3026          * @param  {Element} area   An HTML AREA
       
  3027          * @param  {number} atMost  A number limiting the number of areas to be returned (typically 1 or 0 for no limit)
       
  3028          * @return {MapArea[]}      Array of MapArea objects
       
  3029          */
       
  3030         
       
  3031         getAllDataForArea:function (area,atMost) {
       
  3032             var i,ar, result,
       
  3033                 me=this,
       
  3034                 key = $(area).filter('area').attr(me.options.mapKey);
       
  3035 
       
  3036             if (key) {
       
  3037                 result=[];
       
  3038                 key = u.split(key);
       
  3039 
       
  3040                 for (i=0;i<(atMost || key.length);i++) {
       
  3041                     ar = me.data[me._idFromKey(key[i])];
       
  3042                     ar.area=area.length ? area[0]:area;
       
  3043                     // set the actual area moused over/selected
       
  3044                     // TODO: this is a brittle model for capturing which specific area - if this method was not used,
       
  3045                     // ar.area could have old data. fix this.
       
  3046                     result.push(ar);
       
  3047                 }
       
  3048             }
       
  3049 
       
  3050             return result;
       
  3051         },
       
  3052         getDataForArea: function(area) {
       
  3053             var ar=this.getAllDataForArea(area,1);
       
  3054             return ar ? ar[0] || null : null;
       
  3055         },
       
  3056         getDataForKey: function (key) {
       
  3057             return this.data[this._idFromKey(key)];
       
  3058         },
       
  3059         
       
  3060         /**
       
  3061          * Get the primary keys associated with an area group.
       
  3062          * If this is a primary key, it will be returned.
       
  3063          * 
       
  3064          * @param  {string key An area key
       
  3065          * @return {string} A CSV of area keys
       
  3066          */
       
  3067         
       
  3068         getKeysForGroup: function(key) {
       
  3069             var ar=this.getDataForKey(key);
       
  3070             
       
  3071             return !ar ? '':
       
  3072                 ar.isPrimary ? 
       
  3073                     ar.key :
       
  3074                     this.getPrimaryKeysForMapAreas(ar.areas()).join(',');
       
  3075         },
       
  3076         
       
  3077         /**
       
  3078          * given an array of MapArea object, return an array of its unique primary keys
       
  3079          * @param  {MapArea[]} areas The areas to analyze
       
  3080          * @return {string[]} An array of unique primary keys
       
  3081          */
       
  3082         
       
  3083         getPrimaryKeysForMapAreas: function(areas)
       
  3084         {
       
  3085             var keys=[];
       
  3086             $.each(areas,function(i,e) {
       
  3087                 if ($.inArray(e.keys[0],keys)<0) {
       
  3088                     keys.push(e.keys[0]);
       
  3089                 }
       
  3090             });
       
  3091             return keys;
       
  3092         },
       
  3093         getData: function (obj) {
       
  3094             if (typeof obj === 'string') {
       
  3095                 return this.getDataForKey(obj);
       
  3096             } else if (obj && obj.mapster || u.isElement(obj)) {
       
  3097                 return this.getDataForArea(obj);
       
  3098             } else {
       
  3099                 return null;
       
  3100             }
       
  3101         },
       
  3102         // remove highlight if present, raise event
       
  3103         ensureNoHighlight: function () {
       
  3104             var ar;
       
  3105             if (this.highlightId >= 0) {
       
  3106                 this.graphics.clearHighlight();
       
  3107                 ar = this.data[this.highlightId];
       
  3108                 ar.changeState('highlight', false);
       
  3109                 this.setHighlightId(-1);
       
  3110             }
       
  3111         },
       
  3112         setHighlightId: function(id) {
       
  3113             this.highlightId = id;
       
  3114         },
       
  3115         
       
  3116         /**
       
  3117          * Clear all active selections on this map
       
  3118          */
       
  3119         
       
  3120         clearSelections: function () {
       
  3121             $.each(this.data, function (i,e) {
       
  3122                 if (e.selected) {
       
  3123                     e.deselect(true);
       
  3124                  }
       
  3125             });
       
  3126             this.removeSelectionFinish();
       
  3127             
       
  3128         },
       
  3129 
       
  3130         /**
       
  3131          * Set area options from an array of option data.
       
  3132          * 
       
  3133          * @param {object[]} areas An array of objects containing area-specific options
       
  3134          */
       
  3135         
       
  3136         setAreaOptions: function (areas) {
       
  3137             var i, area_options, ar;
       
  3138             areas = areas || [];
       
  3139 
       
  3140             // refer by: map_data.options[map_data.data[x].area_option_id]
       
  3141             
       
  3142             for (i = areas.length - 1; i >= 0; i--) {
       
  3143                 area_options = areas[i];
       
  3144                 if (area_options) {
       
  3145                     ar = this.getDataForKey(area_options.key);
       
  3146                     if (ar) {
       
  3147                         u.updateProps(ar.options, area_options);
       
  3148                         
       
  3149                         // TODO: will not deselect areas that were previously selected, so this only works
       
  3150                         // for an initial bind.
       
  3151                         
       
  3152                         if (u.isBool(area_options.selected)) {
       
  3153                             ar.selected = area_options.selected;
       
  3154                         }
       
  3155                     }
       
  3156                 }
       
  3157             }
       
  3158         },
       
  3159         // keys: a comma-separated list
       
  3160         drawSelections: function (keys) {
       
  3161             var i, key_arr = u.asArray(keys);
       
  3162 
       
  3163             for (i = key_arr.length - 1; i >= 0; i--) {
       
  3164                 this.data[key_arr[i]].drawSelection();
       
  3165             }
       
  3166         },
       
  3167         redrawSelections: function () {
       
  3168             $.each(this.data, function (i, e) {
       
  3169                 if (e.isSelectedOrStatic()) {
       
  3170                     e.drawSelection();
       
  3171                 }
       
  3172             });
       
  3173 
       
  3174         },
       
  3175         ///called when images are done loading
       
  3176         initialize: function () {
       
  3177             var imgCopy, base_canvas, overlay_canvas, wrap, parentId, css, i,size,
       
  3178                 img,sort_func, sorted_list,  scale,  
       
  3179                         me = this,
       
  3180                         opts = me.options;
       
  3181 
       
  3182             if (me.complete) {
       
  3183                 return;
       
  3184             }
       
  3185 
       
  3186             img = $(me.image);
       
  3187             
       
  3188             parentId = img.parent().attr('id');
       
  3189 
       
  3190             // create a div wrapper only if there's not already a wrapper, otherwise, own it
       
  3191             
       
  3192             if (parentId && parentId.length >= 12 && parentId.substring(0, 12) === "mapster_wrap") {
       
  3193                 wrap = img.parent();
       
  3194                 wrap.attr('id', me.wrapId());
       
  3195             } else {
       
  3196                 wrap = $('<div id="' + me.wrapId() + '"></div>');
       
  3197 
       
  3198                 if (opts.wrapClass) {
       
  3199                     if (opts.wrapClass === true) {
       
  3200                         wrap.addClass(img[0].className);
       
  3201                     }
       
  3202                     else {
       
  3203                         wrap.addClass(opts.wrapClass);
       
  3204                     }
       
  3205                 }
       
  3206             }
       
  3207             me.wrapper = wrap;
       
  3208             
       
  3209             // me.images[1] is the copy of the original image. It should be loaded & at its native size now so we can obtain the true
       
  3210             // width & height. This is needed to scale the imagemap if not being shown at its native size. It is also needed purely
       
  3211             // to finish binding in case the original image was not visible. It can be impossible in some browsers to obtain the
       
  3212             // native size of a hidden image.
       
  3213 
       
  3214             me.scaleInfo = scale = u.scaleMap(me.images[0],me.images[1], opts.scaleMap);
       
  3215             
       
  3216             me.base_canvas = base_canvas = me.graphics.createVisibleCanvas(me);
       
  3217             me.overlay_canvas = overlay_canvas = me.graphics.createVisibleCanvas(me);
       
  3218 
       
  3219             // Now we got what we needed from the copy -clone from the original image again to make sure any other attributes are copied
       
  3220             imgCopy = $(me.images[1])
       
  3221                 .addClass('mapster_el '+ me.images[0].className)
       
  3222                 .attr({id:null, usemap: null});
       
  3223                 
       
  3224             size=u.size(me.images[0]);
       
  3225             
       
  3226             if (size.complete) {
       
  3227                 imgCopy.css({
       
  3228                     width: size.width,
       
  3229                     height: size.height
       
  3230                 });
       
  3231             }
       
  3232      
       
  3233             me.buildDataset();
       
  3234 
       
  3235             // now that we have processed all the areas, set css for wrapper, scale map if needed
       
  3236 
       
  3237             css = {
       
  3238                 display: 'block',
       
  3239                 position: 'relative',
       
  3240                 padding: 0,
       
  3241                 width: scale.width,
       
  3242                 height: scale.height
       
  3243             };
       
  3244 
       
  3245             if (opts.wrapCss) {
       
  3246                 $.extend(css, opts.wrapCss);
       
  3247             }
       
  3248             // if we were rebinding with an existing wrapper, the image will aready be in it
       
  3249             if (img.parent()[0] !== me.wrapper[0]) {
       
  3250 
       
  3251                 img.before(me.wrapper);
       
  3252             }
       
  3253 
       
  3254             wrap.css(css);
       
  3255 
       
  3256             // move all generated images into the wrapper for easy removal later
       
  3257 
       
  3258             $(me.images.slice(2)).hide();
       
  3259             for (i = 1; i < me.images.length; i++) {
       
  3260                 wrap.append(me.images[i]);
       
  3261             }
       
  3262 
       
  3263             //me.images[1].style.cssText = me.image.style.cssText;
       
  3264 
       
  3265             wrap.append(base_canvas)
       
  3266                         .append(overlay_canvas)
       
  3267                         .append(img.css(m.canvas_style));
       
  3268 
       
  3269             // images[0] is the original image with map, images[1] is the copy/background that is visible
       
  3270 
       
  3271             u.setOpacity(me.images[0], 0);
       
  3272             $(me.images[1]).show();
       
  3273 
       
  3274             u.setOpacity(me.images[1],1);
       
  3275 
       
  3276             if (opts.isSelectable && opts.onGetList) {
       
  3277                 sorted_list = me.data.slice(0);
       
  3278                 if (opts.sortList) {
       
  3279                     if (opts.sortList === "desc") {
       
  3280                         sort_func = function (a, b) {
       
  3281                             return a === b ? 0 : (a > b ? -1 : 1);
       
  3282                         };
       
  3283                     }
       
  3284                     else {
       
  3285                         sort_func = function (a, b) {
       
  3286                             return a === b ? 0 : (a < b ? -1 : 1);
       
  3287                         };
       
  3288                     }
       
  3289 
       
  3290                     sorted_list.sort(function (a, b) {
       
  3291                         a = a.value;
       
  3292                         b = b.value;
       
  3293                         return sort_func(a, b);
       
  3294                     });
       
  3295                 }
       
  3296 
       
  3297                 me.options.boundList = opts.onGetList.call(me.image, sorted_list);
       
  3298             }
       
  3299             
       
  3300             me.complete=true;
       
  3301             me.processCommandQueue();
       
  3302             
       
  3303             if (opts.onConfigured && typeof opts.onConfigured === 'function') {
       
  3304                 opts.onConfigured.call(img, true);
       
  3305             }
       
  3306         },
       
  3307 
       
  3308         // when rebind is true, the MapArea data will not be rebuilt.
       
  3309         buildDataset: function(rebind) {
       
  3310             var sel,areas,j,area_id,$area,area,curKey,mapArea,key,keys,mapAreaId,group_value,dataItem,href,
       
  3311                 me=this,
       
  3312                 opts=me.options,
       
  3313                 default_group;
       
  3314 
       
  3315             function addAreaData(key, value) {
       
  3316                 var dataItem = new m.AreaData(me, key, value);
       
  3317                 dataItem.areaId = me._xref[key] = me.data.push(dataItem) - 1;
       
  3318                 return dataItem.areaId;
       
  3319             }
       
  3320 
       
  3321             me._xref = {};
       
  3322             me.data = [];
       
  3323             if (!rebind) {
       
  3324                 me.mapAreas=[];
       
  3325             }
       
  3326 
       
  3327             default_group = !opts.mapKey;
       
  3328             if (default_group) {
       
  3329                 opts.mapKey = 'data-mapster-key';
       
  3330             }
       
  3331 
       
  3332             // the [attribute] selector is broken on old IE with jQuery. hasVml() is a quick and dirty
       
  3333             // way to test for that
       
  3334             
       
  3335             sel = m.hasVml() ? 'area' :
       
  3336                         (default_group ? 
       
  3337                             'area[coords]' : 
       
  3338                             'area[' + opts.mapKey + ']');
       
  3339 
       
  3340             areas = $(me.map).find(sel).unbind('.mapster');
       
  3341                         
       
  3342             for (mapAreaId = 0;mapAreaId<areas.length; mapAreaId++) {
       
  3343                 area_id = 0;
       
  3344                 area = areas[mapAreaId];
       
  3345                 $area = $(area);
       
  3346 
       
  3347                 // skip areas with no coords - selector broken for older ie
       
  3348                 if (!area.coords) {
       
  3349                     continue;
       
  3350                 }
       
  3351                 // Create a key if none was assigned by the user
       
  3352 
       
  3353                 if (default_group) {
       
  3354                      curKey=String(mapAreaId);
       
  3355                     $area.attr('data-mapster-key', curKey);
       
  3356                    
       
  3357                 } else {
       
  3358                     curKey = area.getAttribute(opts.mapKey);
       
  3359                 }
       
  3360 
       
  3361                 // conditions for which the area will be bound to mouse events
       
  3362                 // only bind to areas that don't have nohref. ie 6&7 cannot detect the presence of nohref, so we have to also not bind if href is missing.
       
  3363 
       
  3364                 if (rebind) {
       
  3365                     mapArea = me.mapAreas[$area.data('mapster')-1];
       
  3366                     mapArea.configure(curKey);
       
  3367                 } else {
       
  3368                     mapArea = new m.MapArea(me, area,curKey);
       
  3369                     me.mapAreas.push(mapArea);
       
  3370                 }
       
  3371 
       
  3372                 keys = mapArea.keys; // converted to an array by mapArea
       
  3373 
       
  3374 
       
  3375                 // Iterate through each mapKey assigned to this area
       
  3376                 for (j = keys.length - 1; j >= 0; j--) {
       
  3377                     key = keys[j];
       
  3378 
       
  3379                     if (opts.mapValue) {
       
  3380                         group_value = $area.attr(opts.mapValue);
       
  3381                     }
       
  3382                     if (default_group) {
       
  3383                         // set an attribute so we can refer to the area by index from the DOM object if no key
       
  3384                         area_id = addAreaData(me.data.length, group_value);
       
  3385                         dataItem = me.data[area_id];
       
  3386                         dataItem.key = key = area_id.toString();
       
  3387                     }
       
  3388                     else {
       
  3389                         area_id = me._xref[key];
       
  3390                         if (area_id >= 0) {
       
  3391                             dataItem = me.data[area_id];
       
  3392                             if (group_value && !me.data[area_id].value) {
       
  3393                                 dataItem.value = group_value;
       
  3394                             }
       
  3395                         }
       
  3396                         else {
       
  3397                             area_id = addAreaData(key, group_value);
       
  3398                             dataItem = me.data[area_id];
       
  3399                             dataItem.isPrimary=j===0;
       
  3400                         }
       
  3401                     }
       
  3402                     mapArea.areaDataXref.push(area_id);
       
  3403                     dataItem.areasXref.push(mapAreaId);
       
  3404                 }
       
  3405 
       
  3406                 href=$area.attr('href');
       
  3407                 if (href && href!=='#' && !dataItem.href)
       
  3408                 {
       
  3409                     dataItem.href=href;
       
  3410                 }
       
  3411 
       
  3412                 if (!mapArea.nohref) {
       
  3413                     $area.bind('click.mapster', me.click);
       
  3414                        
       
  3415                     if (!m.isTouch) {
       
  3416                         $area.bind('mouseover.mapster', me.mouseover)
       
  3417                             .bind('mouseout.mapster', me.mouseout)
       
  3418                             .bind('mousedown.mapster', me.mousedown);
       
  3419                         
       
  3420                     }
       
  3421                         
       
  3422                 }
       
  3423 
       
  3424                 // store an ID with each area. 
       
  3425                 $area.data("mapster", mapAreaId+1);
       
  3426             }
       
  3427            
       
  3428             // TODO listenToList
       
  3429             //            if (opts.listenToList && opts.nitG) {
       
  3430             //                opts.nitG.bind('click.mapster', event_hooks[map_data.hooks_index].listclick_hook);
       
  3431             //            }
       
  3432 
       
  3433             // populate areas from config options
       
  3434             me.setAreaOptions(opts.areas);
       
  3435             me.redrawSelections();
       
  3436 
       
  3437         },
       
  3438         processCommandQueue: function() {
       
  3439             
       
  3440             var cur,me=this;
       
  3441             while (!me.currentAction && me.commands.length) {
       
  3442                 cur = me.commands[0];
       
  3443                 me.commands.splice(0,1);
       
  3444                 m.impl[cur.command].apply(cur.that, cur.args);
       
  3445             }
       
  3446         },
       
  3447         clearEvents: function () {
       
  3448             $(this.map).find('area')
       
  3449                         .unbind('.mapster');
       
  3450             $(this.images)
       
  3451                         .unbind('.mapster');
       
  3452         },
       
  3453         _clearCanvases: function (preserveState) {
       
  3454             // remove the canvas elements created
       
  3455             if (!preserveState) {
       
  3456                 $(this.base_canvas).remove();
       
  3457             }
       
  3458             $(this.overlay_canvas).remove();
       
  3459         },
       
  3460         clearMapData: function (preserveState) {
       
  3461             var me = this;
       
  3462             this._clearCanvases(preserveState);
       
  3463 
       
  3464             // release refs to DOM elements
       
  3465             $.each(this.data, function (i, e) {
       
  3466                 e.reset();
       
  3467             });
       
  3468             this.data = null;
       
  3469             if (!preserveState) {
       
  3470                 // get rid of everything except the original image
       
  3471                 this.image.style.cssText = this.imgCssText;
       
  3472                 $(this.wrapper).before(this.image).remove();
       
  3473             }
       
  3474 
       
  3475             me.images.clear();
       
  3476 
       
  3477             this.image = null;
       
  3478             u.ifFunction(this.clearTooltip, this);
       
  3479         },
       
  3480 
       
  3481         // Compelete cleanup process for deslecting items. Called after a batch operation, or by AreaData for single
       
  3482         // operations not flagged as "partial"
       
  3483         
       
  3484         removeSelectionFinish: function () {
       
  3485             var g = this.graphics;
       
  3486 
       
  3487             g.refreshSelections();
       
  3488             // do not call ensure_no_highlight- we don't really want to unhilight it, just remove the effect
       
  3489             g.clearHighlight();
       
  3490         }
       
  3491     };
       
  3492 } (jQuery));
       
  3493 /* areadata.js
       
  3494    AreaData and MapArea protoypes
       
  3495 */
       
  3496 
       
  3497 (function ($) {
       
  3498     var m = $.mapster, u = m.utils;
       
  3499     
       
  3500     /**
       
  3501      * Select this area
       
  3502      * 
       
  3503      * @param {AreaData} me  AreaData context
       
  3504      * @param {object} options Options for rendering the selection
       
  3505      */
       
  3506     function select(options) {
       
  3507         // need to add the new one first so that the double-opacity effect leaves the current one highlighted for singleSelect
       
  3508         
       
  3509         var me=this, o = me.owner;
       
  3510         if (o.options.singleSelect) {
       
  3511             o.clearSelections();
       
  3512         }
       
  3513 
       
  3514         // because areas can overlap - we can't depend on the selection state to tell us anything about the inner areas.
       
  3515         // don't check if it's already selected
       
  3516         if (!me.isSelected()) {
       
  3517             if (options) {
       
  3518                 
       
  3519                 // cache the current options, and map the altImageId if an altimage 
       
  3520                 // was passed
       
  3521 
       
  3522                 me.optsCache = $.extend(me.effectiveRenderOptions('select'),
       
  3523                     options,
       
  3524                     { 
       
  3525                         altImageId: o.images.add(options.altImage)
       
  3526                     });
       
  3527             }
       
  3528 
       
  3529             me.drawSelection();
       
  3530 
       
  3531             me.selected = true;
       
  3532             me.changeState('select', true);
       
  3533         }
       
  3534 
       
  3535         if (o.options.singleSelect) {
       
  3536             o.graphics.refreshSelections();
       
  3537         }
       
  3538     }
       
  3539 
       
  3540     /**
       
  3541      * Deselect this area, optionally deferring finalization so additional areas can be deselected
       
  3542      * in a single operation
       
  3543      * 
       
  3544      * @param  {boolean} partial when true, the caller must invoke "finishRemoveSelection" to render 
       
  3545      */
       
  3546     
       
  3547     function deselect(partial) {
       
  3548         var me=this;
       
  3549         me.selected = false;
       
  3550         me.changeState('select', false);
       
  3551 
       
  3552         // release information about last area options when deselecting.
       
  3553         
       
  3554         me.optsCache=null;
       
  3555         me.owner.graphics.removeSelections(me.areaId);
       
  3556 
       
  3557         // Complete selection removal process. This is separated because it's very inefficient to perform the whole
       
  3558         // process for multiple removals, as the canvas must be totally redrawn at the end of the process.ar.remove
       
  3559         
       
  3560         if (!partial) {
       
  3561             me.owner.removeSelectionFinish();
       
  3562         }
       
  3563     }
       
  3564 
       
  3565     /**
       
  3566      * Toggle the selection state of this area
       
  3567      * @param  {object} options Rendering options, if toggling on
       
  3568      * @return {bool} The new selection state
       
  3569      */
       
  3570     function toggle(options) {
       
  3571         var me=this;
       
  3572         if (!me.isSelected()) {
       
  3573             me.select(options);
       
  3574         }
       
  3575         else {
       
  3576             me.deselect();
       
  3577         }
       
  3578         return me.isSelected();
       
  3579     }
       
  3580 
       
  3581     /**
       
  3582      * An AreaData object; represents a conceptual area that can be composed of 
       
  3583      * one or more MapArea objects
       
  3584      * 
       
  3585      * @param {MapData} owner The MapData object to which this belongs
       
  3586      * @param {string} key   The key for this area
       
  3587      * @param {string} value The mapValue string for this area
       
  3588      */
       
  3589     
       
  3590     m.AreaData = function (owner, key, value) {
       
  3591         $.extend(this,{
       
  3592             owner: owner, 
       
  3593             key: key || '',
       
  3594             // means this represents the first key in a list of keys (it's the area group that gets highlighted on mouseover)
       
  3595             isPrimary: true,
       
  3596             areaId: -1,
       
  3597             href: '',
       
  3598             value: value || '',
       
  3599             options:{},
       
  3600             // "null" means unchanged. Use "isSelected" method to just test true/false 
       
  3601             selected: null,       
       
  3602             // xref to MapArea objects
       
  3603             areasXref: [],
       
  3604             // (temporary storage) - the actual area moused over
       
  3605             area: null,
       
  3606             // the last options used to render this. Cache so when re-drawing after a remove, changes in options won't
       
  3607             // break already selected things. 
       
  3608             optsCache: null
       
  3609          });
       
  3610     };
       
  3611 
       
  3612     /**
       
  3613      * The public API for AreaData object
       
  3614      */
       
  3615 
       
  3616     m.AreaData.prototype = {
       
  3617         constuctor: m.AreaData,
       
  3618         select: select,
       
  3619         deselect: deselect,
       
  3620         toggle: toggle,
       
  3621         areas: function() {
       
  3622             var i,result=[];
       
  3623             for (i=0;i<this.areasXref.length;i++) {
       
  3624                 result.push(this.owner.mapAreas[this.areasXref[i]]);
       
  3625             }
       
  3626             return result;
       
  3627         },
       
  3628         // return all coordinates for all areas
       
  3629         coords: function(offset) {
       
  3630             var coords = [];
       
  3631             $.each(this.areas(), function (i, el) {
       
  3632                 coords = coords.concat(el.coords(offset));
       
  3633             });
       
  3634             return coords;
       
  3635         },
       
  3636         reset: function () {
       
  3637             $.each(this.areas(), function (i, e) {
       
  3638                 e.reset();
       
  3639             });
       
  3640             this.areasXref = [];
       
  3641             this.options = null;
       
  3642         },
       
  3643         // Return the effective selected state of an area, incorporating staticState
       
  3644         isSelectedOrStatic: function () {
       
  3645 
       
  3646             var o = this.effectiveOptions();
       
  3647             return u.isBool(o.staticState) ? o.staticState :
       
  3648                         this.isSelected();
       
  3649         },
       
  3650         isSelected: function () {
       
  3651             return u.isBool(this.selected) ? this.selected :
       
  3652                 u.isBool(this.owner.area_options.selected) ? this.owner.area_options.selected : false;
       
  3653         },
       
  3654         isSelectable: function () {
       
  3655             return u.isBool(this.effectiveOptions().staticState) ? false :
       
  3656                         (u.isBool(this.owner.options.staticState) ? false : u.boolOrDefault(this.effectiveOptions().isSelectable,true));
       
  3657         },
       
  3658         isDeselectable: function () {
       
  3659             return u.isBool(this.effectiveOptions().staticState) ? false :
       
  3660                         (u.isBool(this.owner.options.staticState) ? false : u.boolOrDefault(this.effectiveOptions().isDeselectable,true));
       
  3661         },
       
  3662         isNotRendered: function() {
       
  3663             var area = $(this.area);
       
  3664             return area.attr('nohref') ||
       
  3665                 !area.attr('href') ||
       
  3666                 this.effectiveOptions().isMask;
       
  3667 
       
  3668         },
       
  3669 
       
  3670         
       
  3671          /**
       
  3672          * Return the overall options effective for this area. 
       
  3673          * This should get the default options, and merge in area-specific options, finally
       
  3674          * overlaying options passed by parameter
       
  3675          * 
       
  3676          * @param  {[type]} options  options which will supercede all other options for this area
       
  3677          * @return {[type]}          the combined options
       
  3678          */
       
  3679         
       
  3680         effectiveOptions: function (options) {
       
  3681             
       
  3682             var opts = u.updateProps({},
       
  3683                     this.owner.area_options,
       
  3684                     this.options,
       
  3685                     options || {},
       
  3686                     {
       
  3687                         id: this.areaId 
       
  3688                     }
       
  3689                 );
       
  3690 
       
  3691             opts.selected = this.isSelected();
       
  3692             
       
  3693             return opts;
       
  3694         },
       
  3695 
       
  3696         /**
       
  3697          * Return the options effective for this area for a "render" or "highlight" mode. 
       
  3698          * This should get the default options, merge in the areas-specific options, 
       
  3699          * and then the mode-specific options.
       
  3700          * @param  {string} mode    'render' or 'highlight'
       
  3701          * @param  {[type]} options  options which will supercede all other options for this area
       
  3702          * @return {[type]}          the combined options
       
  3703          */
       
  3704         
       
  3705         effectiveRenderOptions: function (mode, options) {
       
  3706             var allOpts,opts=this.optsCache;
       
  3707             
       
  3708             if (!opts || mode==='highlight') {
       
  3709                 allOpts = this.effectiveOptions(options);
       
  3710                 opts = u.updateProps({},
       
  3711                     allOpts,
       
  3712                     allOpts["render_" + mode]
       
  3713                 );
       
  3714                     
       
  3715                 if (mode!=='highlight') {
       
  3716                     this.optsCache=opts;
       
  3717                 }
       
  3718             }
       
  3719             return $.extend({},opts);
       
  3720         },
       
  3721 
       
  3722         // Fire callback on area state change
       
  3723         changeState: function (state_type, state) {
       
  3724             if ($.isFunction(this.owner.options.onStateChange)) {
       
  3725                 this.owner.options.onStateChange.call(this.owner.image,
       
  3726                     {
       
  3727                         key: this.key,
       
  3728                         state: state_type,
       
  3729                         selected: state
       
  3730                     }
       
  3731                 );
       
  3732             }
       
  3733         },
       
  3734 
       
  3735         // highlight this area
       
  3736          
       
  3737         highlight: function (options) {
       
  3738             var o = this.owner;
       
  3739             if (this.effectiveOptions().highlight) {
       
  3740                 o.graphics.addShapeGroup(this, "highlight",options);
       
  3741             }
       
  3742             o.setHighlightId(this.areaId);
       
  3743             this.changeState('highlight', true);
       
  3744         },
       
  3745 
       
  3746         // select this area. if "callEvent" is true then the state change event will be called. (This method can be used
       
  3747         // during config operations, in which case no event is indicated)
       
  3748         
       
  3749         drawSelection: function () {
       
  3750         
       
  3751 
       
  3752             this.owner.graphics.addShapeGroup(this, "select");
       
  3753         
       
  3754         }
       
  3755 
       
  3756 
       
  3757     };
       
  3758     // represents an HTML area
       
  3759     m.MapArea = function (owner,areaEl,keys) {
       
  3760         if (!owner) {
       
  3761             return;
       
  3762         }
       
  3763         var me = this;
       
  3764         me.owner = owner;   // a MapData object
       
  3765         me.area = areaEl;
       
  3766         me.areaDataXref=[]; // a list of map_data.data[] id's for each areaData object containing this
       
  3767         me.originalCoords = [];
       
  3768         $.each(u.split(areaEl.coords), function (i, el) {
       
  3769             me.originalCoords.push(parseFloat(el));
       
  3770         });
       
  3771         me.length = me.originalCoords.length;
       
  3772         me.shape = areaEl.shape.toLowerCase();
       
  3773         me.nohref = areaEl.nohref || !areaEl.href;
       
  3774         me.configure(keys);
       
  3775     };
       
  3776     m.MapArea.prototype= {
       
  3777         constructor: m.MapArea,
       
  3778         configure: function(keys) {
       
  3779             this.keys = u.split(keys);
       
  3780         },
       
  3781         reset: function() {
       
  3782             this.area=null;
       
  3783         },
       
  3784         coords: function (offset) {
       
  3785             return $.map(this.originalCoords,function(e) {
       
  3786                 return offset ? e : e+offset;
       
  3787             });
       
  3788         }
       
  3789     };
       
  3790 } (jQuery));
       
  3791 /* areacorners.js
       
  3792    determine the best place to put a box of dimensions (width,height) given a circle, rect or poly
       
  3793 */
       
  3794 
       
  3795 (function ($) {
       
  3796     var u=$.mapster.utils;
       
  3797 
       
  3798 
       
  3799     /**
       
  3800      * Compute positions that will place a target with dimensions [width,height] outside 
       
  3801      * but near the boundaries of the elements "elements". When an imagemap is passed, the 
       
  3802      *
       
  3803      * @param  {Element|Element[]} elements An element or an array of elements (such as a jQuery object)
       
  3804      * @param  {Element} image The image to which area elements are bound, if this is an image map.
       
  3805      * @param  {Element} container The contianer in which the target must be constrained (or document, if missing)
       
  3806      * @param  {int} width The width of the target object
       
  3807      * @return {object} a structure with the x and y positions
       
  3808      */
       
  3809     u.areaCorners = function (elements, image, container, width, height) {
       
  3810         var pos,found, minX, minY, maxX, maxY, bestMinX, bestMaxX, bestMinY, bestMaxY, curX, curY, nest, j,
       
  3811            offsetx=0, 
       
  3812            offsety=0,
       
  3813            rootx,
       
  3814            rooty,
       
  3815            iCoords,radius,angle,el,
       
  3816            coords=[];
       
  3817 
       
  3818         // if a single element was passed, map it to an array
       
  3819         
       
  3820         elements = elements.length ? 
       
  3821             elements:
       
  3822             [elements];
       
  3823 
       
  3824         container = container ? 
       
  3825             $(container):
       
  3826             $(document.body);
       
  3827 
       
  3828         // get the relative root of calculation
       
  3829 
       
  3830         pos = container.offset();
       
  3831         rootx = pos.left;
       
  3832         rooty = pos.top;
       
  3833 
       
  3834         // with areas, all we know about is relative to the top-left corner of the image. We need to add an offset compared to
       
  3835         // the actual container. After this calculation, offsetx/offsety can be added to either the area coords, or the target's
       
  3836         // absolute position to get the correct top/left boundaries of the container.
       
  3837 
       
  3838         if (image) {
       
  3839             pos = $(image).offset();
       
  3840             offsetx = pos.left;
       
  3841             offsety = pos.top;
       
  3842         }
       
  3843 
       
  3844         // map the coordinates of any type of shape to a poly and use the logic. simpler than using three different
       
  3845         // calculation methods. Circles use a 20 degree increment for this estimation.
       
  3846 
       
  3847         for (j=0;j<elements.length;j++) 
       
  3848         {
       
  3849             el=elements[j];
       
  3850             if (el.nodeName==='AREA') {
       
  3851                 iCoords = u.split(el.coords,parseInt);
       
  3852 
       
  3853                 switch(el.shape) {
       
  3854                     case 'circle':
       
  3855                         curX=iCoords[0];
       
  3856                         curY=iCoords[1];
       
  3857                         radius=iCoords[2];
       
  3858                         coords=[];
       
  3859                         for (j=0;j<360;j+=20) {
       
  3860                              angle=j*Math.PI/180;
       
  3861                              coords.push(curX+radius*Math.cos(angle),curY+radius*Math.sin(angle));
       
  3862                         }
       
  3863                         break;
       
  3864                       case 'rect':
       
  3865                           coords.push(iCoords[0],iCoords[1],iCoords[2],iCoords[1],iCoords[2],iCoords[3],iCoords[0],iCoords[3]);
       
  3866                           break;
       
  3867                       default:
       
  3868                           coords=coords.concat(iCoords);
       
  3869                          break;
       
  3870                 }
       
  3871 
       
  3872                 // map area positions to it's real position in the container
       
  3873 
       
  3874                 for (j=0;j<coords.length;j+=2)
       
  3875                 {
       
  3876                     coords[j]=parseInt(coords[j],10)+offsetx;
       
  3877                     coords[j+1]=parseInt(coords[j+1],10)+offsety;
       
  3878                 }
       
  3879             } else {
       
  3880                 el=$(el);
       
  3881                 pos = el.position();
       
  3882                 coords.push(pos.left,pos.top,
       
  3883                             pos.left+el.width(),pos.top,
       
  3884                             pos.left+el.width(),pos.top+el.height(),
       
  3885                             pos.left,pos.top+el.height());
       
  3886 
       
  3887             }
       
  3888         }
       
  3889         
       
  3890         minX = minY = bestMinX = bestMinY = 999999;
       
  3891         maxX = maxY = bestMaxX = bestMaxY = -1;
       
  3892 
       
  3893         for (j = coords.length - 2; j >= 0; j -= 2) {
       
  3894             curX = coords[j];
       
  3895             curY = coords[j + 1];
       
  3896             
       
  3897             if (curX < minX) {
       
  3898                 minX = curX;
       
  3899                 bestMaxY = curY;
       
  3900             }
       
  3901             if (curX > maxX) {
       
  3902                 maxX = curX;
       
  3903                 bestMinY = curY;
       
  3904             }
       
  3905             if (curY < minY) {
       
  3906                 minY = curY;
       
  3907                 bestMaxX = curX;
       
  3908             }
       
  3909             if (curY > maxY) {
       
  3910                 maxY = curY;
       
  3911                 bestMinX = curX;
       
  3912             }
       
  3913 
       
  3914         }
       
  3915 
       
  3916         // try to figure out the best place for the tooltip
       
  3917         
       
  3918         if (width && height) {
       
  3919             found=false;
       
  3920             $.each([[bestMaxX - width, minY - height], [bestMinX, minY - height],
       
  3921                              [minX - width, bestMaxY - height], [minX - width, bestMinY],
       
  3922                              [maxX,bestMaxY - height], [ maxX,bestMinY],
       
  3923                              [bestMaxX - width, maxY], [bestMinX, maxY]
       
  3924                       ],function (i, e) {
       
  3925                           if (!found && (e[0] > rootx && e[1] > rooty)) {
       
  3926                               nest = e;
       
  3927                               found=true;
       
  3928                               return false;
       
  3929                   }
       
  3930              });
       
  3931              
       
  3932              // default to lower-right corner if nothing fit inside the boundaries of the image
       
  3933              
       
  3934              if (!found) {
       
  3935                  nest=[maxX,maxY];
       
  3936              }
       
  3937         }
       
  3938         return nest;
       
  3939     };
       
  3940 } (jQuery));
       
  3941 /* scale.js: resize and zoom functionality
       
  3942    requires areacorners.js, when.js
       
  3943 */
       
  3944 
       
  3945 
       
  3946 (function ($) {
       
  3947     var m = $.mapster, u = m.utils, p = m.MapArea.prototype;
       
  3948 
       
  3949     m.utils.getScaleInfo = function (eff, actual) {
       
  3950         var pct;
       
  3951         if (!actual) {
       
  3952             pct = 1;
       
  3953             actual=eff;
       
  3954         } else {
       
  3955             pct = eff.width / actual.width || eff.height / actual.height;
       
  3956             // make sure a float error doesn't muck us up
       
  3957             if (pct > 0.98 && pct < 1.02) { pct = 1; }
       
  3958         }
       
  3959         return {
       
  3960             scale: (pct !== 1),
       
  3961             scalePct: pct,
       
  3962             realWidth: actual.width,
       
  3963             realHeight: actual.height,
       
  3964             width: eff.width,
       
  3965             height: eff.height,
       
  3966             ratio: eff.width / eff.height
       
  3967         };
       
  3968     };
       
  3969     // Scale a set of AREAs, return old data as an array of objects
       
  3970     m.utils.scaleMap = function (image, imageRaw, scale) {
       
  3971         
       
  3972         // stunningly, jQuery width can return zero even as width does not, seems to happen only
       
  3973         // with adBlock or maybe other plugins. These must interfere with onload events somehow.
       
  3974 
       
  3975 
       
  3976         var vis=u.size(image),
       
  3977             raw=u.size(imageRaw,true);
       
  3978 
       
  3979         if (!raw.complete()) {
       
  3980             throw("Another script, such as an extension, appears to be interfering with image loading. Please let us know about this.");
       
  3981         }
       
  3982         if (!vis.complete()) {
       
  3983             vis=raw;
       
  3984         }
       
  3985         return this.getScaleInfo(vis, scale ? raw : null);
       
  3986     };
       
  3987     
       
  3988     /**
       
  3989      * Resize the image map. Only one of newWidth and newHeight should be passed to preserve scale
       
  3990      * 
       
  3991      * @param  {int}   width       The new width OR an object containing named parameters matching this function sig
       
  3992      * @param  {int}   height      The new height
       
  3993      * @param  {int}   effectDuration Time in ms for the resize animation, or zero for no animation
       
  3994      * @param  {function} callback    A function to invoke when the operation finishes
       
  3995      * @return {promise}              NOT YET IMPLEMENTED
       
  3996      */
       
  3997     
       
  3998     m.MapData.prototype.resize = function (width, height, duration, callback) {
       
  3999         var p,promises,newsize,els, highlightId, ratio, 
       
  4000             me = this;
       
  4001         
       
  4002         // allow omitting duration
       
  4003         callback = callback || duration;
       
  4004 
       
  4005         function sizeCanvas(canvas, w, h) {
       
  4006             if (m.hasCanvas()) {
       
  4007                 canvas.width = w;
       
  4008                 canvas.height = h;
       
  4009             } else {
       
  4010                 $(canvas).width(w);
       
  4011                 $(canvas).height(h);
       
  4012             }
       
  4013         }
       
  4014 
       
  4015         // Finalize resize action, do callback, pass control to command queue
       
  4016 
       
  4017         function cleanupAndNotify() {
       
  4018 
       
  4019             me.currentAction = '';
       
  4020             
       
  4021             if ($.isFunction(callback)) {
       
  4022                 callback();
       
  4023             }
       
  4024             
       
  4025             me.processCommandQueue();
       
  4026         }
       
  4027 
       
  4028         // handle cleanup after the inner elements are resized
       
  4029         
       
  4030         function finishResize() {
       
  4031             sizeCanvas(me.overlay_canvas, width, height);
       
  4032 
       
  4033             // restore highlight state if it was highlighted before
       
  4034             if (highlightId >= 0) {
       
  4035                 var areaData = me.data[highlightId];
       
  4036                 areaData.tempOptions = { fade: false };
       
  4037                 me.getDataForKey(areaData.key).highlight();
       
  4038                 areaData.tempOptions = null;
       
  4039             }
       
  4040             sizeCanvas(me.base_canvas, width, height);
       
  4041             me.redrawSelections();
       
  4042             cleanupAndNotify();
       
  4043         }
       
  4044 
       
  4045         function resizeMapData() {
       
  4046             $(me.image).css(newsize);
       
  4047             // start calculation at the same time as effect
       
  4048             me.scaleInfo = u.getScaleInfo({
       
  4049                     width: width,
       
  4050                     height: height
       
  4051                 },
       
  4052                 { 
       
  4053                     width: me.scaleInfo.realWidth,
       
  4054                     height: me.scaleInfo.realHeight
       
  4055                 });
       
  4056             $.each(me.data, function (i, e) {
       
  4057                 $.each(e.areas(), function (i, e) {
       
  4058                     e.resize();
       
  4059                 });
       
  4060             });
       
  4061         }
       
  4062 
       
  4063         if (me.scaleInfo.width === width && me.scaleInfo.height === height) {
       
  4064             return;
       
  4065         }
       
  4066 
       
  4067         highlightId = me.highlightId;
       
  4068 
       
  4069         
       
  4070         if (!width) {
       
  4071             ratio = height / me.scaleInfo.realHeight;
       
  4072             width = Math.round(me.scaleInfo.realWidth * ratio);
       
  4073         }
       
  4074         if (!height) {
       
  4075             ratio = width / me.scaleInfo.realWidth;
       
  4076             height = Math.round(me.scaleInfo.realHeight * ratio);
       
  4077         }
       
  4078 
       
  4079         newsize = { 'width': String(width) + 'px', 'height': String(height) + 'px' };
       
  4080         if (!m.hasCanvas()) {
       
  4081             $(me.base_canvas).children().remove();
       
  4082         }
       
  4083 
       
  4084         // resize all the elements that are part of the map except the image itself (which is not visible)
       
  4085         // but including the div wrapper
       
  4086         els = $(me.wrapper).find('.mapster_el').add(me.wrapper);
       
  4087 
       
  4088         if (duration) {
       
  4089             promises = [];
       
  4090             me.currentAction = 'resizing';
       
  4091             els.each(function (i, e) {
       
  4092                 p = u.defer();
       
  4093                 promises.push(p);
       
  4094 
       
  4095                 $(e).animate(newsize, {
       
  4096                     duration: duration,
       
  4097                     complete: p.resolve,
       
  4098                     easing: "linear"
       
  4099                 });
       
  4100             });
       
  4101 
       
  4102             p = u.defer();
       
  4103             promises.push(p);
       
  4104 
       
  4105             // though resizeMapData is not async, it needs to be finished just the same as the animations,
       
  4106             // so add it to the "to do" list.
       
  4107             
       
  4108             u.when.all(promises).then(finishResize);
       
  4109             resizeMapData();
       
  4110             p.resolve();
       
  4111         } else {
       
  4112             els.css(newsize);
       
  4113             resizeMapData();
       
  4114             finishResize();
       
  4115             
       
  4116         }
       
  4117     };
       
  4118 
       
  4119 
       
  4120     m.MapArea = u.subclass(m.MapArea, function () {
       
  4121         //change the area tag data if needed
       
  4122         this.base.init();
       
  4123         if (this.owner.scaleInfo.scale) {
       
  4124             this.resize();
       
  4125         }
       
  4126     });
       
  4127 
       
  4128     p.coords = function (percent, coordOffset) {
       
  4129         var j, newCoords = [],
       
  4130                     pct = percent || this.owner.scaleInfo.scalePct,
       
  4131                     offset = coordOffset || 0;
       
  4132 
       
  4133         if (pct === 1 && coordOffset === 0) {
       
  4134             return this.originalCoords;
       
  4135         }
       
  4136 
       
  4137         for (j = 0; j < this.length; j++) {
       
  4138             //amount = j % 2 === 0 ? xPct : yPct;
       
  4139             newCoords.push(Math.round(this.originalCoords[j] * pct) + offset);
       
  4140         }
       
  4141         return newCoords;
       
  4142     };
       
  4143     p.resize = function () {
       
  4144         this.area.coords = this.coords().join(',');
       
  4145     };
       
  4146 
       
  4147     p.reset = function () {
       
  4148         this.area.coords = this.coords(1).join(',');
       
  4149     };
       
  4150     
       
  4151     m.impl.resize = function (width, height, duration, callback) {
       
  4152         if (!width && !height) {
       
  4153             return false;
       
  4154         }
       
  4155         var x= (new m.Method(this,
       
  4156                 function () {
       
  4157                     this.resize(width, height, duration, callback);
       
  4158                 },
       
  4159                 null,
       
  4160                 {
       
  4161                     name: 'resize',
       
  4162                     args: arguments
       
  4163                 }
       
  4164             )).go();
       
  4165         return x;
       
  4166     };
       
  4167 
       
  4168 /*
       
  4169     m.impl.zoom = function (key, opts) {
       
  4170         var options = opts || {};
       
  4171 
       
  4172         function zoom(areaData) {
       
  4173             // this will be MapData object returned by Method
       
  4174 
       
  4175             var scroll, corners, height, width, ratio,
       
  4176                     diffX, diffY, ratioX, ratioY, offsetX, offsetY, newWidth, newHeight, scrollLeft, scrollTop,
       
  4177                     padding = options.padding || 0,
       
  4178                     scrollBarSize = areaData ? 20 : 0,
       
  4179                     me = this,
       
  4180                     zoomOut = false;
       
  4181 
       
  4182             if (areaData) {
       
  4183                 // save original state on first zoom operation
       
  4184                 if (!me.zoomed) {
       
  4185                     me.zoomed = true;
       
  4186                     me.preZoomWidth = me.scaleInfo.width;
       
  4187                     me.preZoomHeight = me.scaleInfo.height;
       
  4188                     me.zoomedArea = areaData;
       
  4189                     if (options.scroll) {
       
  4190                         me.wrapper.css({ overflow: 'auto' });
       
  4191                     }
       
  4192                 }
       
  4193                 corners = $.mapster.utils.areaCorners(areaData.coords(1, 0));
       
  4194                 width = me.wrapper.innerWidth() - scrollBarSize - padding * 2;
       
  4195                 height = me.wrapper.innerHeight() - scrollBarSize - padding * 2;
       
  4196                 diffX = corners.maxX - corners.minX;
       
  4197                 diffY = corners.maxY - corners.minY;
       
  4198                 ratioX = width / diffX;
       
  4199                 ratioY = height / diffY;
       
  4200                 ratio = Math.min(ratioX, ratioY);
       
  4201                 offsetX = (width - diffX * ratio) / 2;
       
  4202                 offsetY = (height - diffY * ratio) / 2;
       
  4203 
       
  4204                 newWidth = me.scaleInfo.realWidth * ratio;
       
  4205                 newHeight = me.scaleInfo.realHeight * ratio;
       
  4206                 scrollLeft = (corners.minX) * ratio - padding - offsetX;
       
  4207                 scrollTop = (corners.minY) * ratio - padding - offsetY;
       
  4208             } else {
       
  4209                 if (!me.zoomed) {
       
  4210                     return;
       
  4211                 }
       
  4212                 zoomOut = true;
       
  4213                 newWidth = me.preZoomWidth;
       
  4214                 newHeight = me.preZoomHeight;
       
  4215                 scrollLeft = null;
       
  4216                 scrollTop = null;
       
  4217             }
       
  4218 
       
  4219             this.resize({
       
  4220                 width: newWidth,
       
  4221                 height: newHeight,
       
  4222                 duration: options.duration,
       
  4223                 scroll: scroll,
       
  4224                 scrollLeft: scrollLeft,
       
  4225                 scrollTop: scrollTop,
       
  4226                 // closure so we can be sure values are correct
       
  4227                 callback: (function () {
       
  4228                     var isZoomOut = zoomOut,
       
  4229                             scroll = options.scroll,
       
  4230                             areaD = areaData;
       
  4231                     return function () {
       
  4232                         if (isZoomOut) {
       
  4233                             me.preZoomWidth = null;
       
  4234                             me.preZoomHeight = null;
       
  4235                             me.zoomed = false;
       
  4236                             me.zoomedArea = false;
       
  4237                             if (scroll) {
       
  4238                                 me.wrapper.css({ overflow: 'inherit' });
       
  4239                             }
       
  4240                         } else {
       
  4241                             // just to be sure it wasn't canceled & restarted
       
  4242                             me.zoomedArea = areaD;
       
  4243                         }
       
  4244                     };
       
  4245                 } ())
       
  4246             });
       
  4247         }
       
  4248         return (new m.Method(this,
       
  4249                 function (opts) {
       
  4250                     zoom.call(this);
       
  4251                 },
       
  4252                 function () {
       
  4253                     zoom.call(this.owner, this);
       
  4254                 },
       
  4255                 {
       
  4256                     name: 'zoom',
       
  4257                     args: arguments,
       
  4258                     first: true,
       
  4259                     key: key
       
  4260                 }
       
  4261                 )).go();
       
  4262 
       
  4263 
       
  4264     };
       
  4265     */
       
  4266 } (jQuery));
       
  4267 /* tooltip.js - tooltip functionality
       
  4268    requires areacorners.js
       
  4269 */
       
  4270 
       
  4271 (function ($) {
       
  4272 
       
  4273     var m = $.mapster, u = m.utils;
       
  4274     
       
  4275     $.extend(m.defaults, {
       
  4276         toolTipContainer: '<div style="border: 2px solid black; background: #EEEEEE; width:160px; padding:4px; margin: 4px; -moz-box-shadow: 3px 3px 5px #535353; ' +
       
  4277         '-webkit-box-shadow: 3px 3px 5px #535353; box-shadow: 3px 3px 5px #535353; -moz-border-radius: 6px 6px 6px 6px; -webkit-border-radius: 6px; ' +
       
  4278         'border-radius: 6px 6px 6px 6px; opacity: 0.9;"></dteniv>',
       
  4279         showToolTip: false,
       
  4280         toolTipFade: true,
       
  4281         toolTipClose: ['area-mouseout','image-mouseout'],
       
  4282         onShowToolTip: null,
       
  4283         onHideToolTip: null
       
  4284     });
       
  4285     
       
  4286     $.extend(m.area_defaults, {
       
  4287         toolTip: null,
       
  4288         toolTipClose: null
       
  4289     });
       
  4290     
       
  4291 
       
  4292     /**
       
  4293      * Show a tooltip positioned near this area.
       
  4294      * 
       
  4295      * @param {string|jquery} html A string of html or a jQuery object containing the tooltip content.
       
  4296      * @param {string|jquery} [template] The html template in which to wrap the content
       
  4297      * @param {string|object} [css] CSS to apply to the outermost element of the tooltip 
       
  4298      * @return {jquery} The tooltip that was created
       
  4299      */
       
  4300     
       
  4301     function createToolTip(html, template, css) {
       
  4302         var tooltip;
       
  4303 
       
  4304         // wrap the template in a jQuery object, or clone the template if it's already one.
       
  4305         // This assumes that anything other than a string is a jQuery object; if it's not jQuery will
       
  4306         // probably throw an error.
       
  4307         
       
  4308         if (template) {
       
  4309             tooltip = typeof template === 'string' ?
       
  4310                 $(template) : 
       
  4311                 $(template).clone();
       
  4312 
       
  4313             tooltip.append(html);
       
  4314         } else {
       
  4315             tooltip=$(html);
       
  4316         }
       
  4317 
       
  4318         // always set display to block, or the positioning css won't work if the end user happened to
       
  4319         // use a non-block type element.
       
  4320 
       
  4321         tooltip.css($.extend((css || {}),{
       
  4322                 display:"block",
       
  4323                 position:"absolute"
       
  4324             })).hide();
       
  4325         
       
  4326         $('body').append(tooltip);
       
  4327 
       
  4328         // we must actually add the tooltip to the DOM and "show" it in order to figure out how much space it
       
  4329         // consumes, and then reposition it with that knowledge.
       
  4330         // We also cache the actual opacity setting to restore finally.
       
  4331         
       
  4332         tooltip.attr("data-opacity",tooltip.css("opacity"))
       
  4333             .css("opacity",0);
       
  4334         
       
  4335         // doesn't really show it because opacity=0
       
  4336         
       
  4337         return tooltip.show();
       
  4338     }
       
  4339 
       
  4340 
       
  4341     /**
       
  4342      * Show a tooltip positioned near this area.
       
  4343      * 
       
  4344      * @param {jquery} tooltip The tooltip
       
  4345      * @param {object} [options] options for displaying the tooltip.
       
  4346      *  @config {int} [left] The 0-based absolute x position for the tooltip
       
  4347      *  @config {int} [top] The 0-based absolute y position for the tooltip
       
  4348      *  @config {string|object} [css] CSS to apply to the outermost element of the tooltip 
       
  4349      *  @config {bool} [fadeDuration] When non-zero, the duration in milliseconds of a fade-in effect for the tooltip.
       
  4350      */
       
  4351     
       
  4352     function showToolTipImpl(tooltip,options)
       
  4353     {
       
  4354         var tooltipCss = { 
       
  4355                 "left":  options.left + "px",
       
  4356                 "top": options.top + "px"
       
  4357             }, 
       
  4358             actalOpacity=tooltip.attr("data-opacity") || 0,
       
  4359             zindex = tooltip.css("z-index");
       
  4360         
       
  4361         if (parseInt(zindex,10)===0 
       
  4362             || zindex === "auto") {
       
  4363             tooltipCss["z-index"] = 9999;
       
  4364         }
       
  4365 
       
  4366         tooltip.css(tooltipCss)
       
  4367             .addClass('mapster_tooltip');
       
  4368 
       
  4369         
       
  4370         if (options.fadeDuration && options.fadeDuration>0) {
       
  4371             u.fader(tooltip[0], 0, actalOpacity, options.fadeDuration);
       
  4372         } else {
       
  4373             u.setOpacity(tooltip[0], actalOpacity);
       
  4374         }
       
  4375     }
       
  4376       
       
  4377     /**
       
  4378      * Hide and remove active tooltips
       
  4379      * 
       
  4380      * @param  {MapData} this The mapdata object to which the tooltips belong
       
  4381      */
       
  4382     
       
  4383     m.MapData.prototype.clearToolTip = function() {
       
  4384         if (this.activeToolTip) {
       
  4385             this.activeToolTip.stop().remove();
       
  4386             this.activeToolTip = null;
       
  4387             this.activeToolTipID = null;
       
  4388             u.ifFunction(this.options.onHideToolTip, this);
       
  4389         }
       
  4390     };
       
  4391 
       
  4392     /**
       
  4393      * Configure the binding between a named tooltip closing option, and a mouse event.
       
  4394      *
       
  4395      * If a callback is passed, it will be called when the activating event occurs, and the tooltip will
       
  4396      * only closed if it returns true.
       
  4397      *
       
  4398      * @param  {MapData}  [this]     The MapData object to which this tooltip belongs.
       
  4399      * @param  {String}   option     The name of the tooltip closing option
       
  4400      * @param  {String}   event      UI event to bind to this option
       
  4401      * @param  {Element}  target     The DOM element that is the target of the event
       
  4402      * @param  {Function} [beforeClose] Callback when the tooltip is closed
       
  4403      * @param  {Function} [onClose]  Callback when the tooltip is closed
       
  4404      */
       
  4405     function bindToolTipClose(options, bindOption, event, target, beforeClose, onClose) {
       
  4406         var event_name = event + '.mapster-tooltip';
       
  4407         
       
  4408         if ($.inArray(bindOption, options) >= 0) {
       
  4409             target.unbind(event_name)
       
  4410                 .bind(event_name, function (e) {
       
  4411                     if (!beforeClose || beforeClose.call(this,e)) {
       
  4412                         target.unbind('.mapster-tooltip');
       
  4413                         if (onClose) {
       
  4414                             onClose.call(this);
       
  4415                         }
       
  4416                     }
       
  4417                 });
       
  4418             
       
  4419             return {
       
  4420                 object: target, 
       
  4421                 event: event_name
       
  4422             };
       
  4423         }
       
  4424     }
       
  4425     
       
  4426     /**
       
  4427      * Show a tooltip.
       
  4428      *
       
  4429      * @param {string|jquery}   [tooltip]       A string of html or a jQuery object containing the tooltip content.
       
  4430      * 
       
  4431      * @param {string|jquery}   [target]        The target of the tooltip, to be used to determine positioning. If null,
       
  4432      *                                          absolute position values must be passed with left and top.
       
  4433      *
       
  4434      * @param {string|jquery}   [image]         If target is an [area] the image that owns it
       
  4435      * 
       
  4436      * @param {string|jquery}   [container]     An element within which the tooltip must be bounded
       
  4437      *
       
  4438      *
       
  4439      * 
       
  4440      * @param {object|string|jQuery} [options]  options to apply when creating this tooltip - OR -
       
  4441      *                                          The markup, or a jquery object, containing the data for the tooltip 
       
  4442      *                                         
       
  4443      *  @config {string}        [closeEvents]   A string with one or more comma-separated values that determine when the tooltip
       
  4444      *                                          closes: 'area-click','tooltip-click','image-mouseout' are valid values
       
  4445      *                                          then no template will be used.
       
  4446      *  @config {int}           [offsetx]       the horizontal amount to offset the tooltip 
       
  4447      *  @config {int}           [offsety]       the vertical amount to offset the tooltip 
       
  4448      *  @config {string|object} [css]           CSS to apply to the outermost element of the tooltip 
       
  4449      */
       
  4450     
       
  4451     function showToolTip(tooltip,target,image,container,options) {
       
  4452         var corners,
       
  4453             ttopts = {};
       
  4454     
       
  4455         options = options || {};
       
  4456 
       
  4457 
       
  4458         if (target) {
       
  4459 
       
  4460             corners = u.areaCorners(target,image,container,
       
  4461                                     tooltip.outerWidth(true),
       
  4462                                     tooltip.outerHeight(true));
       
  4463 
       
  4464             // Try to upper-left align it first, if that doesn't work, change the parameters
       
  4465 
       
  4466             ttopts.left = corners[0];
       
  4467             ttopts.top = corners[1];
       
  4468 
       
  4469         } else {
       
  4470             
       
  4471             ttopts.left = options.left;
       
  4472             ttopts.top = options.top;
       
  4473         }
       
  4474 
       
  4475         ttopts.left += (options.offsetx || 0);
       
  4476         ttopts.top +=(options.offsety || 0);
       
  4477 
       
  4478         ttopts.css= options.css;
       
  4479         ttopts.fadeDuration = options.fadeDuration;
       
  4480 
       
  4481         showToolTipImpl(tooltip,ttopts);
       
  4482 
       
  4483         return tooltip;
       
  4484     }
       
  4485     
       
  4486     /**
       
  4487      * Show a tooltip positioned near this area.
       
  4488       *
       
  4489      * @param {string|jquery}   [content]       A string of html or a jQuery object containing the tooltip content.
       
  4490      
       
  4491      * @param {object|string|jQuery} [options]  options to apply when creating this tooltip - OR -
       
  4492      *                                          The markup, or a jquery object, containing the data for the tooltip 
       
  4493      *  @config {string|jquery}   [container]     An element within which the tooltip must be bounded
       
  4494      *  @config {bool}          [template]      a template to use instead of the default. If this property exists and is null,
       
  4495      *                                          then no template will be used.
       
  4496      *  @config {string}        [closeEvents]   A string with one or more comma-separated values that determine when the tooltip
       
  4497      *                                          closes: 'area-click','tooltip-click','image-mouseout' are valid values
       
  4498      *                                          then no template will be used.
       
  4499      *  @config {int}           [offsetx]       the horizontal amount to offset the tooltip 
       
  4500      *  @config {int}           [offsety]       the vertical amount to offset the tooltip 
       
  4501      *  @config {string|object} [css]           CSS to apply to the outermost element of the tooltip 
       
  4502      */
       
  4503     m.AreaData.prototype.showToolTip= function(content,options) {
       
  4504         var tooltip, closeOpts, target, tipClosed, template,
       
  4505             ttopts = {},
       
  4506             ad=this,
       
  4507             md=ad.owner,
       
  4508             areaOpts = ad.effectiveOptions();
       
  4509     
       
  4510         // copy the options object so we can update it
       
  4511         options = options ? $.extend({},options) : {};
       
  4512 
       
  4513         content = content || areaOpts.toolTip;
       
  4514         closeOpts = options.closeEvents || areaOpts.toolTipClose || md.options.toolTipClose || 'tooltip-click';
       
  4515         
       
  4516         template = typeof options.template !== 'undefined' ? 
       
  4517                 options.template :
       
  4518                 md.options.toolTipContainer;
       
  4519 
       
  4520         options.closeEvents = typeof closeOpts === 'string' ?
       
  4521             closeOpts = u.split(closeOpts) :
       
  4522             closeOpts;
       
  4523 
       
  4524         options.fadeDuration = options.fadeDuration ||
       
  4525                  (md.options.toolTipFade ? 
       
  4526                     (md.options.fadeDuration || areaOpts.fadeDuration) : 0);
       
  4527 
       
  4528         target = ad.area ? 
       
  4529             ad.area :
       
  4530             $.map(ad.areas(),
       
  4531                 function(e) {
       
  4532                     return e.area;
       
  4533                 });
       
  4534 
       
  4535         if (md.activeToolTipID===ad.areaId) {
       
  4536             return;
       
  4537         }
       
  4538 
       
  4539         md.clearToolTip();
       
  4540 
       
  4541         md.activeToolTip = tooltip = createToolTip(content,
       
  4542             template,
       
  4543             options.css);
       
  4544 
       
  4545         md.activeToolTipID = ad.areaId;
       
  4546 
       
  4547         tipClosed = function() {
       
  4548             md.clearToolTip();
       
  4549         };
       
  4550 
       
  4551         bindToolTipClose(closeOpts,'area-click', 'click', $(md.map), null, tipClosed);
       
  4552         bindToolTipClose(closeOpts,'tooltip-click', 'click', tooltip,null, tipClosed);
       
  4553         bindToolTipClose(closeOpts,'image-mouseout', 'mouseout', $(md.image), function(e) {
       
  4554             return (e.relatedTarget && e.relatedTarget.nodeName!=='AREA' && e.relatedTarget!==ad.area);
       
  4555         }, tipClosed);
       
  4556 
       
  4557 
       
  4558         showToolTip(tooltip,
       
  4559                     target,
       
  4560                     md.image,
       
  4561                     options.container,
       
  4562                     template,
       
  4563                     options);
       
  4564 
       
  4565         u.ifFunction(md.options.onShowToolTip, ad.area,
       
  4566         {
       
  4567             toolTip: tooltip,
       
  4568             options: ttopts,
       
  4569             areaOptions: areaOpts,
       
  4570             key: ad.key,
       
  4571             selected: ad.isSelected()
       
  4572         });
       
  4573 
       
  4574         return tooltip;
       
  4575     };
       
  4576     
       
  4577 
       
  4578     /**
       
  4579      * Parse an object that could be a string, a jquery object, or an object with a "contents" property
       
  4580      * containing html or a jQuery object.
       
  4581      * 
       
  4582      * @param  {object|string|jQuery} options The parameter to parse
       
  4583      * @return {string|jquery} A string or jquery object
       
  4584      */
       
  4585     function getHtmlFromOptions(options) {
       
  4586 
       
  4587             // see if any html was passed as either the options object itself, or the content property
       
  4588 
       
  4589             return (options ?
       
  4590                 ((typeof options === 'string' || options.jquery) ?
       
  4591                     options :
       
  4592                     options.content) :
       
  4593                 null);
       
  4594     }
       
  4595 
       
  4596     /**
       
  4597      * Activate or remove a tooltip for an area. When this method is called on an area, the
       
  4598      * key parameter doesn't apply and "options" is the first parameter.
       
  4599      *
       
  4600      * When called with no parameters, or "key" is a falsy value, any active tooltip is cleared.
       
  4601      * 
       
  4602      * When only a key is provided, the default tooltip for the area is used. 
       
  4603      * 
       
  4604      * When html is provided, this is used instead of the default tooltip.
       
  4605      * 
       
  4606      * When "noTemplate" is true, the default tooltip template will not be used either, meaning only
       
  4607      * the actual html passed will be used.
       
  4608      *  
       
  4609      * @param  {string|AreaElement} key The area for which to activate a tooltip, or a DOM element.
       
  4610      * 
       
  4611      * @param {object|string|jquery} [options] options to apply when creating this tooltip - OR -
       
  4612      *                                         The markup, or a jquery object, containing the data for the tooltip 
       
  4613      *  @config {string|jQuery} [content]   the inner content of the tooltip; the tooltip text or HTML
       
  4614      *  @config {Element|jQuery} [container]   the inner content of the tooltip; the tooltip text or HTML
       
  4615      *  @config {bool}          [template]  a template to use instead of the default. If this property exists and is null,
       
  4616      *                                      then no template will be used.
       
  4617      *  @config {int}           [offsetx]   the horizontal amount to offset the tooltip.
       
  4618      *  @config {int}           [offsety]   the vertical amount to offset the tooltip.
       
  4619      *  @config {string|object} [css]       CSS to apply to the outermost element of the tooltip 
       
  4620      *  @config {string|object} [css] CSS to apply to the outermost element of the tooltip 
       
  4621      *  @config {bool}          [fadeDuration] When non-zero, the duration in milliseconds of a fade-in effect for the tooltip.
       
  4622      * @return {jQuery} The jQuery object
       
  4623      */
       
  4624     
       
  4625     m.impl.tooltip = function (key,options) {
       
  4626         return (new m.Method(this,
       
  4627         function mapData() {
       
  4628             var tooltip, target, md=this;
       
  4629             if (!key) {
       
  4630                 md.clearToolTip();
       
  4631             } else {
       
  4632                 target=$(key);
       
  4633                 if (md.activeToolTipID ===target[0]) {
       
  4634                     return;
       
  4635                 }
       
  4636                 md.clearToolTip();
       
  4637 
       
  4638                 md.activeToolTip = tooltip = createToolTip(getHtmlFromOptions(options),
       
  4639                             options.template || md.options.toolTipContainer,
       
  4640                             options.css);
       
  4641                 md.activeToolTipID = target[0];
       
  4642 
       
  4643                 bindToolTipClose(['tooltip-click'],'tooltip-click', 'click', tooltip, null, function() {
       
  4644                     md.clearToolTip();
       
  4645                 });
       
  4646 
       
  4647                 md.activeToolTip = tooltip = showToolTip(tooltip,
       
  4648                     target,
       
  4649                     md.image,
       
  4650                     options.container,
       
  4651                     options);
       
  4652             }
       
  4653         },
       
  4654         function areaData() {
       
  4655             if ($.isPlainObject(key) && !options) {
       
  4656                 options = key;
       
  4657             }
       
  4658 
       
  4659             this.showToolTip(getHtmlFromOptions(options),options);
       
  4660         },
       
  4661         { 
       
  4662             name: 'tooltip',
       
  4663             args: arguments,
       
  4664             key: key
       
  4665         }
       
  4666     )).go();
       
  4667     };
       
  4668 } (jQuery));