/* ImageMapster
Version: 1.2.8 (12/30/2012)
Copyright 2011-2012 James Treworgy
http://www.outsharked.com/imagemapster
https://github.com/jamietre/ImageMapster
A jQuery plugin to enhance image maps.
*/
;
/// LICENSE (MIT License)
///
/// Permission is hereby granted, free of charge, to any person obtaining
/// a copy of this software and associated documentation files (the
/// "Software"), to deal in the Software without restriction, including
/// without limitation the rights to use, copy, modify, merge, publish,
/// distribute, sublicense, and/or sell copies of the Software, and to
/// permit persons to whom the Software is furnished to do so, subject to
/// the following conditions:
///
/// The above copyright notice and this permission notice shall be
/// included in all copies or substantial portions of the Software.
///
/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
/// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
/// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
/// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
/// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
/// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
/// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
///
/// January 19, 2011
/** @license MIT License (c) copyright B Cavalier & J Hann */
/**
* when
* A lightweight CommonJS Promises/A and when() implementation
*
* when is part of the cujo.js family of libraries (http://cujojs.com/)
*
* Licensed under the MIT License at:
* http://www.opensource.org/licenses/mit-license.php
*
* @version 1.2.0
*/
/*lint-ignore-start*/
(function (define) {
define(function () {
var freeze, reduceArray, slice, undef;
//
// Public API
//
when.defer = defer;
when.reject = reject;
when.isPromise = isPromise;
when.all = all;
when.some = some;
when.any = any;
when.map = map;
when.reduce = reduce;
when.chain = chain;
/** Object.freeze */
freeze = Object.freeze || function (o) { return o; };
/**
* Trusted Promise constructor. A Promise created from this constructor is
* a trusted when.js promise. Any other duck-typed promise is considered
* untrusted.
*
* @constructor
*/
function Promise() { }
Promise.prototype = freeze({
always: function (alwaysback, progback) {
return this.then(alwaysback, alwaysback, progback);
},
otherwise: function (errback) {
return this.then(undef, errback);
}
});
/**
* Create an already-resolved promise for the supplied value
* @private
*
* @param value anything
* @return {Promise}
*/
function resolved(value) {
var p = new Promise();
p.then = function (callback) {
var nextValue;
try {
if (callback) nextValue = callback(value);
return promise(nextValue === undef ? value : nextValue);
} catch (e) {
return rejected(e);
}
};
return freeze(p);
}
/**
* Create an already-rejected {@link Promise} with the supplied
* rejection reason.
* @private
*
* @param reason rejection reason
* @return {Promise}
*/
function rejected(reason) {
var p = new Promise();
p.then = function (callback, errback) {
var nextValue;
try {
if (errback) {
nextValue = errback(reason);
return promise(nextValue === undef ? reason : nextValue)
}
return rejected(reason);
} catch (e) {
return rejected(e);
}
};
return freeze(p);
}
/**
* Returns a rejected promise for the supplied promiseOrValue. If
* promiseOrValue is a value, it will be the rejection value of the
* returned promise. If promiseOrValue is a promise, its
* completion value will be the rejected value of the returned promise
*
* @param promiseOrValue {*} the rejected value of the returned {@link Promise}
*
* @return {Promise} rejected {@link Promise}
*/
function reject(promiseOrValue) {
return when(promiseOrValue, function (value) {
return rejected(value);
});
}
/**
* Creates a new, CommonJS compliant, Deferred with fully isolated
* resolver and promise parts, either or both of which may be given out
* safely to consumers.
* The Deferred itself has the full API: resolve, reject, progress, and
* then. The resolver has resolve, reject, and progress. The promise
* only has then.
*
* @memberOf when
* @function
*
* @returns {Deferred}
*/
function defer() {
var deferred, promise, listeners, progressHandlers, _then, _progress, complete;
listeners = [];
progressHandlers = [];
/**
* Pre-resolution then() that adds the supplied callback, errback, and progback
* functions to the registered listeners
*
* @private
*
* @param [callback] {Function} resolution handler
* @param [errback] {Function} rejection handler
* @param [progback] {Function} progress handler
*
* @throws {Error} if any argument is not null, undefined, or a Function
*/
_then = function unresolvedThen(callback, errback, progback) {
var deferred = defer();
listeners.push(function (promise) {
promise.then(callback, errback)
.then(deferred.resolve, deferred.reject, deferred.progress);
});
progback && progressHandlers.push(progback);
return deferred.promise;
};
/**
* Registers a handler for this {@link Deferred}'s {@link Promise}. Even though all arguments
* are optional, each argument that *is* supplied must be null, undefined, or a Function.
* Any other value will cause an Error to be thrown.
*
* @memberOf Promise
*
* @param [callback] {Function} resolution handler
* @param [errback] {Function} rejection handler
* @param [progback] {Function} progress handler
*
* @throws {Error} if any argument is not null, undefined, or a Function
*/
function then(callback, errback, progback) {
return _then(callback, errback, progback);
}
/**
* Resolves this {@link Deferred}'s {@link Promise} with val as the
* resolution value.
*
* @memberOf Resolver
*
* @param val anything
*/
function resolve(val) {
complete(resolved(val));
}
/**
* Rejects this {@link Deferred}'s {@link Promise} with err as the
* reason.
*
* @memberOf Resolver
*
* @param err anything
*/
function reject(err) {
complete(rejected(err));
}
/**
* @private
* @param update
*/
_progress = function (update) {
var progress, i = 0;
while (progress = progressHandlers[i++]) progress(update);
};
/**
* Emits a progress update to all progress observers registered with
* this {@link Deferred}'s {@link Promise}
*
* @memberOf Resolver
*
* @param update anything
*/
function progress(update) {
_progress(update);
}
/**
* Transition from pre-resolution state to post-resolution state, notifying
* all listeners of the resolution or rejection
*
* @private
*
* @param completed {Promise} the completed value of this deferred
*/
complete = function (completed) {
var listener, i = 0;
// Replace _then with one that directly notifies with the result.
_then = completed.then;
// Replace complete so that this Deferred can only be completed
// once. Also Replace _progress, so that subsequent attempts to issue
// progress throw.
complete = _progress = function alreadyCompleted() {
// TODO: Consider silently returning here so that parties who
// have a reference to the resolver cannot tell that the promise
// has been resolved using try/catch
throw new Error("already completed");
};
// Free progressHandlers array since we'll never issue progress events
// for this promise again now that it's completed
progressHandlers = undef;
// Notify listeners
// Traverse all listeners registered directly with this Deferred
while (listener = listeners[i++]) {
listener(completed);
}
listeners = [];
};
/**
* The full Deferred object, with both {@link Promise} and {@link Resolver}
* parts
* @class Deferred
* @name Deferred
*/
deferred = {};
// Promise and Resolver parts
// Freeze Promise and Resolver APIs
promise = new Promise();
promise.then = deferred.then = then;
/**
* The {@link Promise} for this {@link Deferred}
* @memberOf Deferred
* @name promise
* @type {Promise}
*/
deferred.promise = freeze(promise);
/**
* The {@link Resolver} for this {@link Deferred}
* @memberOf Deferred
* @name resolver
* @class Resolver
*/
deferred.resolver = freeze({
resolve: (deferred.resolve = resolve),
reject: (deferred.reject = reject),
progress: (deferred.progress = progress)
});
return deferred;
}
/**
* Determines if promiseOrValue is a promise or not. Uses the feature
* test from http://wiki.commonjs.org/wiki/Promises/A to determine if
* promiseOrValue is a promise.
*
* @param promiseOrValue anything
*
* @returns {Boolean} true if promiseOrValue is a {@link Promise}
*/
function isPromise(promiseOrValue) {
return promiseOrValue && typeof promiseOrValue.then === 'function';
}
/**
* Register an observer for a promise or immediate value.
*
* @function
* @name when
* @namespace
*
* @param promiseOrValue anything
* @param {Function} [callback] callback to be called when promiseOrValue is
* successfully resolved. If promiseOrValue is an immediate value, callback
* will be invoked immediately.
* @param {Function} [errback] callback to be called when promiseOrValue is
* rejected.
* @param {Function} [progressHandler] callback to be called when progress updates
* are issued for promiseOrValue.
*
* @returns {Promise} a new {@link Promise} that will complete with the return
* value of callback or errback or the completion value of promiseOrValue if
* callback and/or errback is not supplied.
*/
function when(promiseOrValue, callback, errback, progressHandler) {
// Get a promise for the input promiseOrValue
// See promise()
var trustedPromise = promise(promiseOrValue);
// Register promise handlers
return trustedPromise.then(callback, errback, progressHandler);
}
/**
* Returns promiseOrValue if promiseOrValue is a {@link Promise}, a new Promise if
* promiseOrValue is a foreign promise, or a new, already-resolved {@link Promise}
* whose resolution value is promiseOrValue if promiseOrValue is an immediate value.
*
* Note that this function is not safe to export since it will return its
* input when promiseOrValue is a {@link Promise}
*
* @private
*
* @param promiseOrValue anything
*
* @returns Guaranteed to return a trusted Promise. If promiseOrValue is a when.js {@link Promise}
* returns promiseOrValue, otherwise, returns a new, already-resolved, when.js {@link Promise}
* whose resolution value is:
* * the resolution value of promiseOrValue if it's a foreign promise, or
* * promiseOrValue if it's a value
*/
function promise(promiseOrValue) {
var promise, deferred;
if (promiseOrValue instanceof Promise) {
// It's a when.js promise, so we trust it
promise = promiseOrValue;
} else {
// It's not a when.js promise. Check to see if it's a foreign promise
// or a value.
deferred = defer();
if (isPromise(promiseOrValue)) {
// It's a compliant promise, but we don't know where it came from,
// so we don't trust its implementation entirely. Introduce a trusted
// middleman when.js promise
// IMPORTANT: This is the only place when.js should ever call .then() on
// an untrusted promise.
promiseOrValue.then(deferred.resolve, deferred.reject, deferred.progress);
promise = deferred.promise;
} else {
// It's a value, not a promise. Create an already-resolved promise
// for it.
deferred.resolve(promiseOrValue);
promise = deferred.promise;
}
}
return promise;
}
/**
* Return a promise that will resolve when howMany of the supplied promisesOrValues
* have resolved. The resolution value of the returned promise will be an array of
* length howMany containing the resolutions values of the triggering promisesOrValues.
*
* @memberOf when
*
* @param promisesOrValues {Array} array of anything, may contain a mix
* of {@link Promise}s and values
* @param howMany
* @param [callback]
* @param [errback]
* @param [progressHandler]
*
* @returns {Promise}
*/
function some(promisesOrValues, howMany, callback, errback, progressHandler) {
checkCallbacks(2, arguments);
return when(promisesOrValues, function (promisesOrValues) {
var toResolve, results, ret, deferred, resolver, rejecter, handleProgress, len, i;
len = promisesOrValues.length >>> 0;
toResolve = Math.max(0, Math.min(howMany, len));
results = [];
deferred = defer();
ret = when(deferred, callback, errback, progressHandler);
// Wrapper so that resolver can be replaced
function resolve(val) {
resolver(val);
}
// Wrapper so that rejecter can be replaced
function reject(err) {
rejecter(err);
}
// Wrapper so that progress can be replaced
function progress(update) {
handleProgress(update);
}
function complete() {
resolver = rejecter = handleProgress = noop;
}
// No items in the input, resolve immediately
if (!toResolve) {
deferred.resolve(results);
} else {
// Resolver for promises. Captures the value and resolves
// the returned promise when toResolve reaches zero.
// Overwrites resolver var with a noop once promise has
// be resolved to cover case where n < promises.length
resolver = function (val) {
// This orders the values based on promise resolution order
// Another strategy would be to use the original position of
// the corresponding promise.
results.push(val);
if (! --toResolve) {
complete();
deferred.resolve(results);
}
};
// Rejecter for promises. Rejects returned promise
// immediately, and overwrites rejecter var with a noop
// once promise to cover case where n < promises.length.
// TODO: Consider rejecting only when N (or promises.length - N?)
// promises have been rejected instead of only one?
rejecter = function (err) {
complete();
deferred.reject(err);
};
handleProgress = deferred.progress;
// TODO: Replace while with forEach
for (i = 0; i < len; ++i) {
if (i in promisesOrValues) {
when(promisesOrValues[i], resolve, reject, progress);
}
}
}
return ret;
});
}
/**
* Return a promise that will resolve only once all the supplied promisesOrValues
* have resolved. The resolution value of the returned promise will be an array
* containing the resolution values of each of the promisesOrValues.
*
* @memberOf when
*
* @param promisesOrValues {Array|Promise} array of anything, may contain a mix
* of {@link Promise}s and values
* @param [callback] {Function}
* @param [errback] {Function}
* @param [progressHandler] {Function}
*
* @returns {Promise}
*/
function all(promisesOrValues, callback, errback, progressHandler) {
checkCallbacks(1, arguments);
return when(promisesOrValues, function (promisesOrValues) {
return _reduce(promisesOrValues, reduceIntoArray, []);
}).then(callback, errback, progressHandler);
}
function reduceIntoArray(current, val, i) {
current[i] = val;
return current;
}
/**
* Return a promise that will resolve when any one of the supplied promisesOrValues
* has resolved. The resolution value of the returned promise will be the resolution
* value of the triggering promiseOrValue.
*
* @memberOf when
*
* @param promisesOrValues {Array|Promise} array of anything, may contain a mix
* of {@link Promise}s and values
* @param [callback] {Function}
* @param [errback] {Function}
* @param [progressHandler] {Function}
*
* @returns {Promise}
*/
function any(promisesOrValues, callback, errback, progressHandler) {
function unwrapSingleResult(val) {
return callback ? callback(val[0]) : val[0];
}
return some(promisesOrValues, 1, unwrapSingleResult, errback, progressHandler);
}
/**
* Traditional map function, similar to `Array.prototype.map()`, but allows
* input to contain {@link Promise}s and/or values, and mapFunc may return
* either a value or a {@link Promise}
*
* @memberOf when
*
* @param promise {Array|Promise} array of anything, may contain a mix
* of {@link Promise}s and values
* @param mapFunc {Function} mapping function mapFunc(value) which may return
* either a {@link Promise} or value
*
* @returns {Promise} a {@link Promise} that will resolve to an array containing
* the mapped output values.
*/
function map(promise, mapFunc) {
return when(promise, function (array) {
return _map(array, mapFunc);
});
}
/**
* Private map helper to map an array of promises
* @private
*
* @param promisesOrValues {Array}
* @param mapFunc {Function}
* @return {Promise}
*/
function _map(promisesOrValues, mapFunc) {
var results, len, i;
// Since we know the resulting length, we can preallocate the results
// array to avoid array expansions.
len = promisesOrValues.length >>> 0;
results = new Array(len);
// Since mapFunc may be async, get all invocations of it into flight
// asap, and then use reduce() to collect all the results
for (i = 0; i < len; i++) {
if (i in promisesOrValues)
results[i] = when(promisesOrValues[i], mapFunc);
}
// Could use all() here, but that would result in another array
// being allocated, i.e. map() would end up allocating 2 arrays
// of size len instead of just 1. Since all() uses reduce()
// anyway, avoid the additional allocation by calling reduce
// directly.
return _reduce(results, reduceIntoArray, results);
}
/**
* Traditional reduce function, similar to `Array.prototype.reduce()`, but
* input may contain {@link Promise}s and/or values, and reduceFunc
* may return either a value or a {@link Promise}, *and* initialValue may
* be a {@link Promise} for the starting value.
*
* @memberOf when
*
* @param promise {Array|Promise} array of anything, may contain a mix
* of {@link Promise}s and values. May also be a {@link Promise} for
* an array.
* @param reduceFunc {Function} reduce function reduce(currentValue, nextValue, index, total),
* where total is the total number of items being reduced, and will be the same
* in each call to reduceFunc.
* @param initialValue starting value, or a {@link Promise} for the starting value
*
* @returns {Promise} that will resolve to the final reduced value
*/
function reduce(promise, reduceFunc, initialValue) {
var args = slice.call(arguments, 1);
return when(promise, function (array) {
return _reduce.apply(undef, [array].concat(args));
});
}
/**
* Private reduce to reduce an array of promises
* @private
*
* @param promisesOrValues {Array}
* @param reduceFunc {Function}
* @param initialValue {*}
* @return {Promise}
*/
function _reduce(promisesOrValues, reduceFunc, initialValue) {
var total, args;
total = promisesOrValues.length;
// Skip promisesOrValues, since it will be used as 'this' in the call
// to the actual reduce engine below.
// Wrap the supplied reduceFunc with one that handles promises and then
// delegates to the supplied.
args = [
function (current, val, i) {
return when(current, function (c) {
return when(val, function (value) {
return reduceFunc(c, value, i, total);
});
});
}
];
if (arguments.length > 2) args.push(initialValue);
return reduceArray.apply(promisesOrValues, args);
}
/**
* Ensure that resolution of promiseOrValue will complete resolver with the completion
* value of promiseOrValue, or instead with resolveValue if it is provided.
*
* @memberOf when
*
* @param promiseOrValue
* @param resolver {Resolver}
* @param [resolveValue] anything
*
* @returns {Promise}
*/
function chain(promiseOrValue, resolver, resolveValue) {
var useResolveValue = arguments.length > 2;
return when(promiseOrValue,
function (val) {
if (useResolveValue) val = resolveValue;
resolver.resolve(val);
return val;
},
function (e) {
resolver.reject(e);
return rejected(e);
},
resolver.progress
);
}
//
// Utility functions
//
/**
* Helper that checks arrayOfCallbacks to ensure that each element is either
* a function, or null or undefined.
*
* @private
*
* @param arrayOfCallbacks {Array} array to check
* @throws {Error} if any element of arrayOfCallbacks is something other than
* a Functions, null, or undefined.
*/
function checkCallbacks(start, arrayOfCallbacks) {
var arg, i = arrayOfCallbacks.length;
while (i > start) {
arg = arrayOfCallbacks[--i];
if (arg != null && typeof arg != 'function') throw new Error('callback is not a function');
}
}
/**
* No-Op function used in method replacement
* @private
*/
function noop() { }
slice = [].slice;
// ES5 reduce implementation if native not available
// See: http://es5.github.com/#x15.4.4.21 as there are many
// specifics and edge cases.
reduceArray = [].reduce ||
function (reduceFunc /*, initialValue */) {
// ES5 dictates that reduce.length === 1
// This implementation deviates from ES5 spec in the following ways:
// 1. It does not check if reduceFunc is a Callable
var arr, args, reduced, len, i;
i = 0;
arr = Object(this);
len = arr.length >>> 0;
args = arguments;
// If no initialValue, use first item of array (we know length !== 0 here)
// and adjust i to start at second item
if (args.length <= 1) {
// Skip to the first real element in the array
for (; ; ) {
if (i in arr) {
reduced = arr[i++];
break;
}
// If we reached the end of the array without finding any real
// elements, it's a TypeError
if (++i >= len) {
throw new TypeError();
}
}
} else {
// If initialValue provided, use it
reduced = args[1];
}
// Do the actual reduce
for (; i < len; ++i) {
// Skip holes
if (i in arr)
reduced = reduceFunc(reduced, arr[i], i, arr);
}
return reduced;
};
return when;
});
})(typeof define == 'function'
? define
: function (factory) {
typeof module != 'undefined'
? (module.exports = factory())
: (jQuery.mapster_when = factory());
}
// Boilerplate for AMD, Node, and browser global
);
/*lint-ignore-end*/
/* ImageMapster core */
/*jslint laxbreak: true, evil: true, unparam: true */
/*global jQuery: true, Zepto: true */
(function ($) {
// all public functions in $.mapster.impl are methods
$.fn.mapster = function (method) {
var m = $.mapster.impl;
if ($.isFunction(m[method])) {
return m[method].apply(this, Array.prototype.slice.call(arguments, 1));
} else if (typeof method === 'object' || !method) {
return m.bind.apply(this, arguments);
} else {
$.error('Method ' + method + ' does not exist on jQuery.mapster');
}
};
$.mapster = {
version: "1.2.8",
render_defaults: {
isSelectable: true,
isDeselectable: true,
fade: false,
fadeDuration: 150,
fill: true,
fillColor: '000000',
fillColorMask: 'FFFFFF',
fillOpacity: 0.7,
highlight: true,
stroke: false,
strokeColor: 'ff0000',
strokeOpacity: 1,
strokeWidth: 1,
includeKeys: '',
altImage: null,
altImageId: null, // used internally
altImages: {}
},
defaults: {
clickNavigate: false,
wrapClass: null,
wrapCss: null,
onGetList: null,
sortList: false,
listenToList: false,
mapKey: '',
mapValue: '',
singleSelect: false,
listKey: 'value',
listSelectedAttribute: 'selected',
listSelectedClass: null,
onClick: null,
onMouseover: null,
onMouseout: null,
mouseoutDelay: 0,
onStateChange: null,
boundList: null,
onConfigured: null,
configTimeout: 30000,
noHrefIsMask: true,
scaleMap: true,
safeLoad: false,
areas: []
},
shared_defaults: {
render_highlight: { fade: true },
render_select: { fade: false },
staticState: null,
selected: null
},
area_defaults:
{
includeKeys: '',
isMask: false
},
canvas_style: {
position: 'absolute',
left: 0,
top: 0,
padding: 0,
border: 0
},
hasCanvas: null,
isTouch: null,
windowLoaded: false,
map_cache: [],
hooks: {},
addHook: function(name,callback) {
this.hooks[name]=(this.hooks[name]||[]).push(callback);
},
callHooks: function(name,context) {
$.each(this.hooks[name]||[],function(i,e) {
e.apply(context);
});
},
utils: {
when: $.mapster_when,
defer: $.mapster_when.defer,
// extends the constructor, returns a new object prototype. Does not refer to the
// original constructor so is protected if the original object is altered. This way you
// can "extend" an object by replacing it with its subclass.
subclass: function(BaseClass, constr) {
var Subclass=function() {
var me=this,
args=Array.prototype.slice.call(arguments,0);
me.base = BaseClass.prototype;
me.base.init = function() {
BaseClass.prototype.constructor.apply(me,args);
};
constr.apply(me,args);
};
Subclass.prototype = new BaseClass();
Subclass.prototype.constructor=Subclass;
return Subclass;
},
asArray: function (obj) {
return obj.constructor === Array ?
obj : this.split(obj);
},
// clean split: no padding or empty elements
split: function (text,cb) {
var i,el, arr = text.split(',');
for (i = 0; i < arr.length; i++) {
el = $.trim(arr[i]);
if (el==='') {
arr.splice(i,1);
} else {
arr[i] = cb ? cb(el):el;
}
}
return arr;
},
// similar to $.extend but does not add properties (only updates), unless the
// first argument is an empty object, then all properties will be copied
updateProps: function (_target, _template) {
var onlyProps,
target = _target || {},
template = $.isEmptyObject(target) ? _template : _target;
//if (template) {
onlyProps = [];
$.each(template, function (prop) {
onlyProps.push(prop);
});
//}
$.each(Array.prototype.slice.call(arguments, 1), function (i, src) {
$.each(src || {}, function (prop) {
if (!onlyProps || $.inArray(prop, onlyProps) >= 0) {
var p = src[prop];
if ($.isPlainObject(p)) {
// not recursive - only copies 1 level of subobjects, and always merges
target[prop] = $.extend(target[prop] || {}, p);
} else if (p && p.constructor === Array) {
target[prop] = p.slice(0);
} else if (typeof p !== 'undefined') {
target[prop] = src[prop];
}
}
});
});
return target;
},
isElement: function (o) {
return (typeof HTMLElement === "object" ? o instanceof HTMLElement :
o && typeof o === "object" && o.nodeType === 1 && typeof o.nodeName === "string");
},
// finds element of array or object with a property "prop" having value "val"
// if prop is not defined, then just looks for property with value "val"
indexOfProp: function (obj, prop, val) {
var result = obj.constructor === Array ? -1 : null;
$.each(obj, function (i, e) {
if (e && (prop ? e[prop] : e) === val) {
result = i;
return false;
}
});
return result;
},
// returns "obj" if true or false, or "def" if not true/false
boolOrDefault: function (obj, def) {
return this.isBool(obj) ?
obj : def || false;
},
isBool: function (obj) {
return typeof obj === "boolean";
},
isUndef: function(obj) {
return typeof obj === "undefined";
},
// evaluates "obj", if function, calls it with args
// (todo - update this to handle variable lenght/more than one arg)
ifFunction: function (obj, that, args) {
if ($.isFunction(obj)) {
obj.call(that, args);
}
},
size: function(image, raw) {
var u=$.mapster.utils;
return {
width: raw ? (image.width || image.naturalWidth) : u.imgWidth(image,true) ,
height: raw ? (image.height || image.naturalHeight) : u.imgHeight(image,true),
complete: function() { return !!this.height && !!this.width;}
};
},
// basic function to set the opacity of an element.
// this gets monkey patched by the graphics module when running in IE6-8
setOpacity: function (el, opacity) {
el.style.opacity = opacity;
},
// fade "el" from opacity "op" to "endOp" over a period of time "duration"
fader: (function () {
var elements = {},
lastKey = 0,
fade_func = function (el, op, endOp, duration) {
var index,
cbIntervals = duration/15,
obj, u = $.mapster.utils;
if (typeof el === 'number') {
obj = elements[el];
if (!obj) {
return;
}
} else {
index = u.indexOfProp(elements, null, el);
if (index) {
delete elements[index];
}
elements[++lastKey] = obj = el;
el = lastKey;
}
endOp = endOp || 1;
op = (op + (endOp / cbIntervals) > endOp - 0.01) ? endOp : op + (endOp / cbIntervals);
u.setOpacity(obj, op);
if (op < endOp) {
setTimeout(function () {
fade_func(el, op, endOp, duration);
}, 15);
}
};
return fade_func;
} ())
},
getBoundList: function (opts, key_list) {
if (!opts.boundList) {
return null;
}
var index, key, result = $(), list = $.mapster.utils.split(key_list);
opts.boundList.each(function (i,e) {
for (index = 0; index < list.length; index++) {
key = list[index];
if ($(e).is('[' + opts.listKey + '="' + key + '"]')) {
result = result.add(e);
}
}
});
return result;
},
// Causes changes to the bound list based on the user action (select or deselect)
// area: the jQuery area object
// returns the matching elements from the bound list for the first area passed (normally only one should be passed, but
// a list can be passed
setBoundListProperties: function (opts, target, selected) {
target.each(function (i,e) {
if (opts.listSelectedClass) {
if (selected) {
$(e).addClass(opts.listSelectedClass);
} else {
$(e).removeClass(opts.listSelectedClass);
}
}
if (opts.listSelectedAttribute) {
$(e).attr(opts.listSelectedAttribute, selected);
}
});
},
getMapDataIndex: function (obj) {
var img, id;
switch (obj.tagName && obj.tagName.toLowerCase()) {
case 'area':
id = $(obj).parent().attr('name');
img = $("img[usemap='#" + id + "']")[0];
break;
case 'img':
img = obj;
break;
}
return img ?
this.utils.indexOfProp(this.map_cache, 'image', img) : -1;
},
getMapData: function (obj) {
var index = this.getMapDataIndex(obj.length ? obj[0]:obj);
if (index >= 0) {
return index >= 0 ? this.map_cache[index] : null;
}
},
queueCommand: function (map_data, that, command, args) {
if (!map_data) {
return false;
}
if (!map_data.complete || map_data.currentAction) {
map_data.commands.push(
{
that: that,
command: command,
args: args
});
return true;
}
return false;
},
unload: function () {
this.impl.unload();
this.utils = null;
this.impl = null;
$.fn.mapster = null;
$.mapster = null;
$('*').unbind();
}
};
// Config for object prototypes
// first: use only first object (for things that should not apply to lists)
/// calls back one of two fuinctions, depending on whether an area was obtained.
// opts: {
// name: 'method name',
// key: 'key,
// args: 'args'
//
//}
// name: name of method (required)
// args: arguments to re-call with
// Iterates through all the objects passed, and determines whether it's an area or an image, and calls the appropriate
// callback for each. If anything is returned from that callback, the process is stopped and that data return. Otherwise,
// the object itself is returned.
var m = $.mapster,
u = m.utils,
ap = Array.prototype;
// jQuery's width() and height() are broken on IE9 in some situations. This tries everything.
$.each(["width","height"],function(i,e) {
var capProp = e.substr(0,1).toUpperCase() + e.substr(1);
// when jqwidth parm is passed, it also checks the jQuery width()/height() property
// the issue is that jQUery width() can report a valid size before the image is loaded in some browsers
// without it, we can read zero even when image is loaded in other browsers if its not visible
// we must still check because stuff like adblock can temporarily block it
// what a goddamn headache
u["img"+capProp]=function(img,jqwidth) {
return (jqwidth ? $(img)[e]() : 0) ||
img[e] || img["natural"+capProp] || img["client"+capProp] || img["offset"+capProp];
};
});
m.Method = function (that, func_map, func_area, opts) {
var me = this;
me.name = opts.name;
me.output = that;
me.input = that;
me.first = opts.first || false;
me.args = opts.args ? ap.slice.call(opts.args, 0) : [];
me.key = opts.key;
me.func_map = func_map;
me.func_area = func_area;
//$.extend(me, opts);
me.name = opts.name;
me.allowAsync = opts.allowAsync || false;
};
m.Method.prototype.go = function () {
var i, data, ar, len, result, src = this.input,
area_list = [],
me = this;
len = src.length;
for (i = 0; i < len; i++) {
data = $.mapster.getMapData(src[i]);
if (data) {
if (!me.allowAsync && m.queueCommand(data, me.input, me.name, me.args)) {
if (this.first) {
result = '';
}
continue;
}
ar = data.getData(src[i].nodeName === 'AREA' ? src[i] : this.key);
if (ar) {
if ($.inArray(ar, area_list) < 0) {
area_list.push(ar);
}
} else {
result = this.func_map.apply(data, me.args);
}
if (this.first || typeof result !== 'undefined') {
break;
}
}
}
// if there were areas, call the area function for each unique group
$(area_list).each(function (i,e) {
result = me.func_area.apply(e, me.args);
});
if (typeof result !== 'undefined') {
return result;
} else {
return this.output;
}
};
$.mapster.impl = (function () {
var me = {},
removeMap, addMap;
addMap = function (map_data) {
return m.map_cache.push(map_data) - 1;
};
removeMap = function (map_data) {
m.map_cache.splice(map_data.index, 1);
for (var i = m.map_cache.length - 1; i >= this.index; i--) {
m.map_cache[i].index--;
}
};
/// return current map_data for an image or area
// merge new area data into existing area options. used for rebinding.
function merge_areas(map_data, areas) {
var ar, index,
map_areas = map_data.options.areas;
if (areas) {
$.each(areas, function (i, e) {
// Issue #68 - ignore invalid data in areas array
if (!e || !e.key) {
return;
}
index = u.indexOfProp(map_areas, "key", e.key);
if (index >= 0) {
$.extend(map_areas[index], e);
}
else {
map_areas.push(e);
}
ar = map_data.getDataForKey(e.key);
if (ar) {
$.extend(ar.options, e);
}
});
}
}
function merge_options(map_data, options) {
var temp_opts = u.updateProps({}, options);
delete temp_opts.areas;
u.updateProps(map_data.options, temp_opts);
merge_areas(map_data, options.areas);
// refresh the area_option template
u.updateProps(map_data.area_options, map_data.options);
}
// Most methods use the "Method" object which handles figuring out whether it's an image or area called and
// parsing key parameters. The constructor wants:
// this, the jQuery object
// a function that is called when an image was passed (with a this context of the MapData)
// a function that is called when an area was passed (with a this context of the AreaData)
// options: first = true means only the first member of a jQuery object is handled
// key = the key parameters passed
// defaultReturn: a value to return other than the jQuery object (if its not chainable)
// args: the arguments
// Returns a comma-separated list of user-selected areas. "staticState" areas are not considered selected for the purposes of this method.
me.get = function (key) {
var md = m.getMapData(this);
if (!(md && md.complete)) {
throw("Can't access data until binding complete.");
}
return (new m.Method(this,
function () {
// map_data return
return this.getSelected();
},
function () {
return this.isSelected();
},
{ name: 'get',
args: arguments,
key: key,
first: true,
allowAsync: true,
defaultReturn: ''
}
)).go();
};
me.data = function (key) {
return (new m.Method(this,
null,
function () {
return this;
},
{ name: 'data',
args: arguments,
key: key
}
)).go();
};
// Set or return highlight state.
// $(img).mapster('highlight') -- return highlighted area key, or null if none
// $(area).mapster('highlight') -- highlight an area
// $(img).mapster('highlight','area_key') -- highlight an area
// $(img).mapster('highlight',false) -- remove highlight
me.highlight = function (key) {
return (new m.Method(this,
function () {
if (key === false) {
this.ensureNoHighlight();
} else {
var id = this.highlightId;
return id >= 0 ? this.data[id].key : null;
}
},
function () {
this.highlight();
},
{ name: 'highlight',
args: arguments,
key: key,
first: true
}
)).go();
};
// Return the primary keys for an area or group key.
// $(area).mapster('key')
// includes all keys (not just primary keys)
// $(area).mapster('key',true)
// $(img).mapster('key','group-key')
// $(img).mapster('key','group-key', true)
me.keys = function(key,all) {
var keyList=[],
md = m.getMapData(this);
if (!(md && md.complete)) {
throw("Can't access data until binding complete.");
}
function addUniqueKeys(ad) {
var areas,keys=[];
if (!all) {
keys.push(ad.key);
} else {
areas=ad.areas();
$.each(areas,function(i,e) {
keys=keys.concat(e.keys);
});
}
$.each(keys,function(i,e) {
if ($.inArray(e,keyList)<0) {
keyList.push(e);
}
});
}
if (!(md && md.complete)) {
return '';
}
if (typeof key === 'string') {
if (all) {
addUniqueKeys(md.getDataForKey(key));
} else {
keyList=[md.getKeysForGroup(key)];
}
} else {
all = key;
this.each(function(i,e) {
if (e.nodeName==='AREA') {
addUniqueKeys(md.getDataForArea(e));
}
});
}
return keyList.join(',');
};
me.select = function () {
me.set.call(this, true);
};
me.deselect = function () {
me.set.call(this, false);
};
/**
* Select or unselect areas. Areas can be identified by a single string key, a comma-separated list of keys,
* or an array of strings.
*
*
* @param {boolean} selected Determines whether areas are selected or deselected
* @param {string|string[]} key A string, comma-separated string, or array of strings indicating
* the areas to select or deselect
* @param {object} options Rendering options to apply when selecting an area
*/
me.set = function (selected, key, options) {
var lastMap, map_data, opts=options,
key_list, area_list; // array of unique areas passed
function setSelection(ar) {
if (ar) {
switch (selected) {
case true:
ar.select(opts); break;
case false:
ar.deselect(true); break;
default:
ar.toggle(opts); break;
}
}
}
function addArea(ar) {
if (ar && $.inArray(ar, area_list) < 0) {
area_list.push(ar);
key_list+=(key_list===''?'':',')+ar.key;
}
}
// Clean up after a group that applied to the same map
function finishSetForMap(map_data) {
$.each(area_list, function (i, el) {
setSelection(el);
});
if (!selected) {
map_data.removeSelectionFinish();
}
if (map_data.options.boundList) {
m.setBoundListProperties(map_data.options, m.getBoundList(map_data.options, key_list), selected);
}
}
this.filter('img,area').each(function (i,e) {
var keys;
map_data = m.getMapData(e);
if (map_data !== lastMap) {
if (lastMap) {
finishSetForMap(lastMap);
}
area_list = [];
key_list='';
}
if (map_data) {
keys = '';
if (e.nodeName.toUpperCase()==='IMG') {
if (!m.queueCommand(map_data, $(e), 'set', [selected, key, opts])) {
if (key instanceof Array) {
if (key.length) {
keys = key.join(",");
}
}
else {
keys = key;
}
if (keys) {
$.each(u.split(keys), function (i,key) {
addArea(map_data.getDataForKey(key.toString()));
lastMap = map_data;
});
}
}
} else {
opts=key;
if (!m.queueCommand(map_data, $(e), 'set', [selected, opts])) {
addArea(map_data.getDataForArea(e));
lastMap = map_data;
}
}
}
});
if (map_data) {
finishSetForMap(map_data);
}
return this;
};
me.unbind = function (preserveState) {
return (new m.Method(this,
function () {
this.clearEvents();
this.clearMapData(preserveState);
removeMap(this);
},
null,
{ name: 'unbind',
args: arguments
}
)).go();
};
// refresh options and update selection information.
me.rebind = function (options) {
return (new m.Method(this,
function () {
var me=this;
me.complete=false;
me.configureOptions(options);
me.bindImages().then(function() {
me.buildDataset(true);
me.complete=true;
});
//this.redrawSelections();
},
null,
{
name: 'rebind',
args: arguments
}
)).go();
};
// get options. nothing or false to get, or "true" to get effective options (versus passed options)
me.get_options = function (key, effective) {
var eff = u.isBool(key) ? key : effective; // allow 2nd parm as "effective" when no key
return (new m.Method(this,
function () {
var opts = $.extend({}, this.options);
if (eff) {
opts.render_select = u.updateProps(
{},
m.render_defaults,
opts,
opts.render_select);
opts.render_highlight = u.updateProps(
{},
m.render_defaults,
opts,
opts.render_highlight);
}
return opts;
},
function () {
return eff ? this.effectiveOptions() : this.options;
},
{
name: 'get_options',
args: arguments,
first: true,
allowAsync: true,
key: key
}
)).go();
};
// set options - pass an object with options to set,
me.set_options = function (options) {
return (new m.Method(this,
function () {
merge_options(this, options);
},
null,
{
name: 'set_options',
args: arguments
}
)).go();
};
me.unload = function () {
var i;
for (i = m.map_cache.length - 1; i >= 0; i--) {
if (m.map_cache[i]) {
me.unbind.call($(m.map_cache[i].image));
}
}
me.graphics = null;
};
me.snapshot = function () {
return (new m.Method(this,
function () {
$.each(this.data, function (i, e) {
e.selected = false;
});
this.base_canvas = this.graphics.createVisibleCanvas(this);
$(this.image).before(this.base_canvas);
},
null,
{ name: 'snapshot' }
)).go();
};
// do not queue this function
me.state = function () {
var md, result = null;
$(this).each(function (i,e) {
if (e.nodeName === 'IMG') {
md = m.getMapData(e);
if (md) {
result = md.state();
}
return false;
}
});
return result;
};
me.bind = function (options) {
return this.each(function (i,e) {
var img, map, usemap, md;
// save ref to this image even if we can't access it yet. commands will be queued
img = $(e);
md = m.getMapData(e);
// if already bound completely, do a total rebind
if (md) {
me.unbind.apply(img);
if (!md.complete) {
// will be queued
img.bind();
return true;
}
md = null;
}
// ensure it's a valid image
// jQuery bug with Opera, results in full-url#usemap being returned from jQuery's attr.
// So use raw getAttribute instead.
usemap = this.getAttribute('usemap');
map = usemap && $('map[name="' + usemap.substr(1) + '"]');
if (!(img.is('img') && usemap && map.size() > 0)) {
return true;
}
// sorry - your image must have border:0, things are too unpredictable otherwise.
img.css('border', 0);
if (!md) {
md = new m.MapData(this, options);
md.index = addMap(md);
md.map = map;
md.bindImages().then(function() {
md.initialize();
});
}
});
};
me.init = function (useCanvas) {
var style, shapes;
// check for excanvas explicitly - don't be fooled
m.hasCanvas = (document.namespaces && document.namespaces.g_vml_) ? false :
$('')[0].getContext ? true : false;
m.isTouch = 'ontouchstart' in document.documentElement;
if (!(m.hasCanvas || document.namespaces)) {
$.fn.mapster = function () {
return this;
};
return;
}
$.extend(m.defaults, m.render_defaults,m.shared_defaults);
$.extend(m.area_defaults, m.render_defaults,m.shared_defaults);
// for testing/debugging, use of canvas can be forced by initializing manually with "true" or "false"
if (u.isBool(useCanvas)) {
m.hasCanvas = useCanvas;
}
if ($.browser.msie && !m.hasCanvas && !document.namespaces.v) {
document.namespaces.add("v", "urn:schemas-microsoft-com:vml");
style = document.createStyleSheet();
shapes = ['shape', 'rect', 'oval', 'circ', 'fill', 'stroke', 'imagedata', 'group', 'textbox'];
$.each(shapes,
function (i, el) {
style.addRule('v\\:' + el, "behavior: url(#default#VML); antialias:true");
});
}
};
me.test = function (obj) {
return eval(obj);
};
return me;
} ());
$.mapster.impl.init();
} (jQuery));
/* graphics.js
Graphics object handles all rendering.
*/
(function ($) {
var p, m=$.mapster,
u=m.utils;
/**
* Implemenation to add each area in an AreaData object to the canvas
* @param {Graphics} graphics The target graphics object
* @param {AreaData} areaData The AreaData object (a collection of area elements and metadata)
* @param {object} options Rendering options to apply when rendering this group of areas
*/
function addShapeGroupImpl(graphics, areaData, options) {
var me = graphics,
md = me.map_data,
isMask = options.isMask;
// first get area options. Then override fade for selecting, and finally merge in the
// "select" effect options.
$.each(areaData.areas(), function (i,e) {
options.isMask = isMask || (e.nohref && md.options.noHrefIsMask);
me.addShape(e, options);
});
// it's faster just to manipulate the passed options isMask property and restore it, than to
// copy the object each time
options.isMask=isMask;
}
/**
* An object associated with a particular map_data instance to manage renderin.
* @param {MapData} map_data The MapData object bound to this instance
*/
m.Graphics = function (map_data) {
//$(window).unload($.mapster.unload);
// create graphics functions for canvas and vml browsers. usage:
// 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
// 3) call add_shape_to for each shape or mask, 4) call render() to finish
var me = this;
me.active = false;
me.canvas = null;
me.width = 0;
me.height = 0;
me.shapes = [];
me.masks = [];
me.map_data = map_data;
};
p = m.Graphics.prototype= {
constructor: m.Graphics,
/**
* Initiate a graphics request for a canvas
* @param {Element} canvas The canvas element that is the target of this operation
* @param {string} [elementName] The name to assign to the element (VML only)
*/
begin: function(canvas, elementName) {
var c = $(canvas);
this.elementName = elementName;
this.canvas = canvas;
this.width = c.width();
this.height = c.height();
this.shapes = [];
this.masks = [];
this.active = true;
},
/**
* Add an area to be rendered to this canvas.
* @param {MapArea} mapArea The MapArea object to render
* @param {object} options An object containing any rendering options that should override the
* defaults for the area
*/
addShape: function(mapArea, options) {
var addto = options.isMask ? this.masks : this.shapes;
addto.push({ mapArea: mapArea, options: options });
},
/**
* Create a canvas that is sized and styled for the MapData object
* @param {MapData} mapData The MapData object that will receive this new canvas
* @return {Element} A canvas element
*/
createVisibleCanvas: function (mapData) {
return $(this.createCanvasFor(mapData))
.addClass('mapster_el')
.css(m.canvas_style)[0];
},
/**
* Add a group of shapes from an AreaData object to the canvas
*
* @param {AreaData} areaData An AreaData object (a set of area elements)
* @param {string} mode The rendering mode, "select" or "highlight". This determines the target
* canvas and which default options to use.
* @param {striong} options Rendering options
*/
addShapeGroup: function (areaData, mode,options) {
// render includeKeys first - because they could be masks
var me = this,
list, name, canvas,
map_data = this.map_data,
opts = areaData.effectiveRenderOptions(mode);
if (options) {
$.extend(opts,options);
}
if (mode === 'select') {
name = "static_" + areaData.areaId.toString();
canvas = map_data.base_canvas;
} else {
canvas = map_data.overlay_canvas;
}
me.begin(canvas, name);
if (opts.includeKeys) {
list = u.split(opts.includeKeys);
$.each(list, function (i,e) {
var areaData = map_data.getDataForKey(e.toString());
addShapeGroupImpl(me,areaData, areaData.effectiveRenderOptions(mode));
});
}
addShapeGroupImpl(me,areaData, opts);
me.render();
if (opts.fade) {
// fading requires special handling for IE. We must access the fill elements directly. The fader also has to deal with
// the "opacity" attribute (not css)
u.fader(m.hasCanvas ?
canvas :
$(canvas).find('._fill').not('.mapster_mask'),
0,
m.hasCanvas ?
1 :
opts.fillOpacity,
opts.fadeDuration);
}
}
};
// configure remaining prototype methods for ie or canvas-supporting browser
if (m.hasCanvas) {
/**
* Convert a hex value to decimal
* @param {string} hex A hexadecimal string
* @return {int} Integer represenation of the hex string
*/
p.hex_to_decimal = function (hex) {
return Math.max(0, Math.min(parseInt(hex, 16), 255));
};
p.css3color = function (color, opacity) {
return 'rgba(' + this.hex_to_decimal(color.substr(0, 2)) + ','
+ this.hex_to_decimal(color.substr(2, 2)) + ','
+ this.hex_to_decimal(color.substr(4, 2)) + ',' + opacity + ')';
};
p.renderShape = function (context, mapArea, offset) {
var i,
c = mapArea.coords(null,offset);
switch (mapArea.shape) {
case 'rect':
context.rect(c[0], c[1], c[2] - c[0], c[3] - c[1]);
break;
case 'poly':
context.moveTo(c[0], c[1]);
for (i = 2; i < mapArea.length; i += 2) {
context.lineTo(c[i], c[i + 1]);
}
context.lineTo(c[0], c[1]);
break;
case 'circ':
case 'circle':
context.arc(c[0], c[1], c[2], 0, Math.PI * 2, false);
break;
}
};
p.addAltImage = function (context, image, mapArea, options) {
context.beginPath();
this.renderShape(context, mapArea);
context.closePath();
context.clip();
context.globalAlpha = options.altImageOpacity || options.fillOpacity;
context.drawImage(image, 0, 0, mapArea.owner.scaleInfo.width, mapArea.owner.scaleInfo.height);
};
p.render = function () {
// firefox 6.0 context.save() seems to be broken. to work around, we have to draw the contents on one temp canvas,
// the mask on another, and merge everything. ugh. fixed in 1.2.2. unfortunately this is a lot more code for masks,
// but no other way around it that i can see.
var maskCanvas, maskContext,
me = this,
md = me.map_data,
hasMasks = me.masks.length,
shapeCanvas = me.createCanvasFor(md),
shapeContext = shapeCanvas.getContext('2d'),
context = me.canvas.getContext('2d');
if (hasMasks) {
maskCanvas = me.createCanvasFor(md);
maskContext = maskCanvas.getContext('2d');
maskContext.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
$.each(me.masks, function (i,e) {
maskContext.save();
maskContext.beginPath();
me.renderShape(maskContext, e.mapArea);
maskContext.closePath();
maskContext.clip();
maskContext.lineWidth = 0;
maskContext.fillStyle = '#000';
maskContext.fill();
maskContext.restore();
});
}
$.each(me.shapes, function (i,s) {
shapeContext.save();
if (s.options.fill) {
if (s.options.altImageId) {
me.addAltImage(shapeContext, md.images[s.options.altImageId], s.mapArea, s.options);
} else {
shapeContext.beginPath();
me.renderShape(shapeContext, s.mapArea);
shapeContext.closePath();
//shapeContext.clip();
shapeContext.fillStyle = me.css3color(s.options.fillColor, s.options.fillOpacity);
shapeContext.fill();
}
}
shapeContext.restore();
});
// render strokes at end since masks get stroked too
$.each(me.shapes.concat(me.masks), function (i,s) {
var offset = s.options.strokeWidth === 1 ? 0.5 : 0;
// offset applies only when stroke width is 1 and stroke would render between pixels.
if (s.options.stroke) {
shapeContext.save();
shapeContext.strokeStyle = me.css3color(s.options.strokeColor, s.options.strokeOpacity);
shapeContext.lineWidth = s.options.strokeWidth;
shapeContext.beginPath();
me.renderShape(shapeContext, s.mapArea, offset);
shapeContext.closePath();
shapeContext.stroke();
shapeContext.restore();
}
});
if (hasMasks) {
// render the new shapes against the mask
maskContext.globalCompositeOperation = "source-out";
maskContext.drawImage(shapeCanvas, 0, 0);
// flatten into the main canvas
context.drawImage(maskCanvas, 0, 0);
} else {
context.drawImage(shapeCanvas, 0, 0);
}
me.active = false;
return me.canvas;
};
// create a canvas mimicing dimensions of an existing element
p.createCanvasFor = function (md) {
return $('')[0];
};
p.clearHighlight = function () {
var c = this.map_data.overlay_canvas;
c.getContext('2d').clearRect(0, 0, c.width, c.height);
};
p.removeSelections = function () {
};
// 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.
p.refreshSelections = function () {
var canvas_temp, map_data = this.map_data;
// draw new base canvas, then swap with the old one to avoid flickering
canvas_temp = map_data.base_canvas;
map_data.base_canvas = this.createVisibleCanvas(map_data);
$(map_data.base_canvas).hide();
$(canvas_temp).before(map_data.base_canvas);
map_data.redrawSelections();
$(map_data.base_canvas).show();
$(canvas_temp).remove();
};
} else {
/**
* Set the opacity of the element. This is an IE<8 specific function for handling VML.
* When using VML we must override the "setOpacity" utility function (monkey patch ourselves).
* jQuery does not deal with opacity correctly for VML elements. This deals with that.
*
* @param {Element} el The DOM element
* @param {double} opacity A value between 0 and 1 inclusive.
*/
u.setOpacity = function(el,opacity) {
$(el).each(function(i,e) {
if (typeof e.opacity !=='undefined') {
e.opacity=opacity;
} else {
$(e).css("opacity",opacity);
}
});
};
p.renderShape = function (mapArea, options, cssclass) {
var me = this, fill,stroke, e, t_fill, el_name, el_class, template, c = mapArea.coords();
el_name = me.elementName ? 'name="' + me.elementName + '" ' : '';
el_class = cssclass ? 'class="' + cssclass + '" ' : '';
t_fill = '')
.addClass('mapster_el')
.hide();
index=me._add(image[0]);
image
.bind('load',function(e) {
me.imageLoaded.call(me,e);
})
.bind('error',function(e) {
me.imageLoadError.call(me,e);
});
image.attr('src', src);
} else {
// use attr because we want the actual source, not the resolved path the browser will return directly calling image.src
index=me._add($(image)[0]);
}
if (id) {
if (this[id]) {
throw(id+" is already used or is not available as an altImage alias.");
}
me.ids.push(id);
me[id]=me[index];
}
return index;
},
/**
* Bind the images in this object,
* @param {boolean} retry when true, indicates that the function is calling itself after failure
* @return {Promise} a promise that resolves when the images have finished loading
*/
bind: function(retry) {
var me = this,
promise,
triesLeft = me.owner.options.configTimeout / 200,
/* A recursive function to continue checking that the images have been
loaded until a timeout has elapsed */
check=function() {
var i;
// refresh status of images
i=me.length;
while (i-->0) {
if (!me.isLoaded(i)) {
break;
}
}
// check to see if every image has already been loaded
if (me.complete()) {
me.resolve();
} else {
// to account for failure of onLoad to fire in rare situations
if (triesLeft-- > 0) {
me.imgTimeout=window.setTimeout(function() {
check.call(me,true);
}, 50);
} else {
me.imageLoadError.call(me);
}
}
};
promise = me.deferred=u.defer();
check();
return promise;
},
resolve: function() {
var me=this,
resolver=me.deferred;
if (resolver) {
// Make a copy of the resolver before calling & removing it to ensure
// it is not called twice
me.deferred=null;
resolver.resolve();
}
},
/**
* Event handler for image onload
* @param {object} e jQuery event data
*/
imageLoaded: function(e) {
var me=this,
index = me.indexOf(e.target);
if (index>=0) {
me.status[index] = true;
if ($.inArray(false, me.status) < 0) {
me.resolve();
}
}
},
/**
* Event handler for onload error
* @param {object} e jQuery event data
*/
imageLoadError: function(e) {
clearTimeout(this.imgTimeout);
this.triesLeft=0;
var err = e ? 'The image ' + e.target.src + ' failed to load.' :
'The images never seemed to finish loading. You may just need to increase the configTimeout if images could take a long time to load.';
throw err;
},
/**
* Test if the image at specificed index has finished loading
* @param {int} index The image index
* @return {boolean} true if loaded, false if not
*/
isLoaded: function(index) {
var img,
me=this,
status=me.status;
if (status[index]) { return true; }
img = me[index];
if (typeof img.complete !== 'undefined') {
status[index]=img.complete;
} else {
status[index]=!!u.imgWidth(img);
}
// if complete passes, the image is loaded, but may STILL not be available because of stuff like adblock.
// make sure it is.
return status[index];
}
};
} (jQuery));
/* mapdata.js
the MapData object, repesents an instance of a single bound imagemap
*/
(function ($) {
var m = $.mapster,
u = m.utils;
/**
* Set default values for MapData object properties
* @param {MapData} me The MapData object
*/
function initializeDefaults(me) {
$.extend(me,{
complete: false, // (bool) when configuration is complete
map: null, // ($) the image map
base_canvas: null, // (canvas|var) where selections are rendered
overlay_canvas: null, // (canvas|var) where highlights are rendered
commands: [], // {} commands that were run before configuration was completed (b/c images weren't loaded)
data: [], // MapData[] area groups
mapAreas: [], // MapArea[] list. AreaData entities contain refs to this array, so options are stored with each.
_xref: {}, // (int) xref of mapKeys to data[]
highlightId: -1, // (int) the currently highlighted element.
currentAreaId: -1,
_tooltip_events: [], // {} info on events we bound to a tooltip container, so we can properly unbind them
scaleInfo: null, // {} info about the image size, scaling, defaults
index: -1, // index of this in map_cache - so we have an ID to use for wraper div
activeAreaEvent: null
});
}
/**
* Return an array of all image-containing options from an options object;
* that is, containers that may have an "altImage" property
*
* @param {object} obj An options object
* @return {object[]} An array of objects
*/
function getOptionImages(obj) {
return [obj, obj.render_highlight, obj.render_select];
}
/**
* Parse all the altImage references, adding them to the library so they can be preloaded
* and aliased.
*
* @param {MapData} me The MapData object on which to operate
*/
function configureAltImages(me)
{
var opts = me.options,
mi = me.images;
// add alt images
if ($.mapster.hasCanvas) {
// map altImage library first
$.each(opts.altImages || {}, function(i,e) {
mi.add(e,i);
});
// now find everything else
$.each([opts].concat(opts.areas),function(i,e) {
$.each(getOptionImages(e),function(i2,e2) {
if (e2 && e2.altImage) {
e2.altImageId=mi.add(e2.altImage);
}
});
});
}
// set area_options
me.area_options = u.updateProps({}, // default options for any MapArea
m.area_defaults,
opts);
}
/**
* Queue a mouse move action based on current delay settings
* (helper for mouseover/mouseout handlers)
*
* @param {MapData} me The MapData context
* @param {number} delay The number of milliseconds to delay the action
* @param {AreaData} area AreaData affected
* @param {Deferred} deferred A deferred object to return (instead of a new one)
* @return {Promise} A promise that resolves when the action is completed
*/
function queueMouseEvent(me,delay,area, deferred) {
deferred = deferred || u.when.defer();
function cbFinal(areaId) {
if (me.currentAreaId!==areaId && me.highlightId>=0) {
deferred.resolve();
}
}
if (me.activeAreaEvent) {
window.clearTimeout(me.activeAreaEvent);
me.activeAreaEvent=0;
}
if (delay<0) {
return;
}
if (area.owner.currentAction || delay) {
me.activeAreaEvent = window.setTimeout((function() {
return function() {
queueMouseEvent(me,0,area,deferred);
};
}(area)),
delay || 100);
} else {
cbFinal(area.areaId);
}
return deferred;
}
/**
* Mousedown event. This is captured only to prevent browser from drawing an outline around an
* area when it's clicked.
*
* @param {EventData} e jQuery event data
*/
function mousedown(e) {
if (!$.mapster.hasCanvas) {
this.blur();
}
e.preventDefault();
}
/**
* Mouseover event. Handle highlight rendering and client callback on mouseover
*
* @param {MapData} me The MapData context
* @param {EventData} e jQuery event data
* @return {[type]} [description]
*/
function mouseover(me,e) {
var arData = me.getAllDataForArea(this),
ar=arData.length ? arData[0] : null;
// mouseover events are ignored entirely while resizing, though we do care about mouseout events
// and must queue the action to keep things clean.
if (!ar || ar.isNotRendered() || ar.owner.currentAction) {
return;
}
if (me.currentAreaId === ar.areaId) {
return;
}
if (me.highlightId !== ar.areaId) {
me.clearEffects();
ar.highlight();
if (me.options.showToolTip) {
$.each(arData,function(i,e) {
if (e.effectiveOptions().toolTip) {
e.showToolTip();
}
});
}
}
me.currentAreaId = ar.areaId;
if ($.isFunction(me.options.onMouseover)) {
me.options.onMouseover.call(this,
{
e: e,
options:ar.effectiveOptions(),
key: ar.key,
selected: ar.isSelected()
});
}
}
/**
* Mouseout event.
*
* @param {MapData} me The MapData context
* @param {EventData} e jQuery event data
* @return {[type]} [description]
*/
function mouseout(me,e) {
var newArea,
ar = me.getDataForArea(this),
opts = me.options;
if (me.currentAreaId<0 || !ar) {
return;
}
newArea=me.getDataForArea(e.relatedTarget);
if (newArea === ar) {
return;
}
me.currentAreaId = -1;
ar.area=null;
queueMouseEvent(me,opts.mouseoutDelay,ar)
.then(me.clearEffects);
if ($.isFunction(opts.onMouseout)) {
opts.onMouseout.call(this,
{
e: e,
options: opts,
key: ar.key,
selected: ar.isSelected()
});
}
}
/**
* Clear any active tooltip or highlight
*
* @param {MapData} me The MapData context
* @param {EventData} e jQuery event data
* @return {[type]} [description]
*/
function clearEffects(me) {
var opts = me.options;
me.ensureNoHighlight();
if (opts.toolTipClose
&& $.inArray('area-mouseout', opts.toolTipClose) >= 0
&& me.activeToolTip)
{
me.clearToolTip();
}
}
/**
* Mouse click event handler
*
* @param {MapData} me The MapData context
* @param {EventData} e jQuery event data
* @return {[type]} [description]
*/
function click(me,e) {
var selected, list, list_target, newSelectionState, canChangeState, cbResult,
that = this,
ar = me.getDataForArea(this),
opts = me.options;
function clickArea(ar) {
var areaOpts,target;
canChangeState = (ar.isSelectable() &&
(ar.isDeselectable() || !ar.isSelected()));
if (canChangeState) {
newSelectionState = !ar.isSelected();
} else {
newSelectionState = ar.isSelected();
}
list_target = m.getBoundList(opts, ar.key);
if ($.isFunction(opts.onClick))
{
cbResult= opts.onClick.call(that,
{
e: e,
listTarget: list_target,
key: ar.key,
selected: newSelectionState
});
if (u.isBool(cbResult)) {
if (!cbResult) {
return false;
}
target = $(ar.area).attr('href');
if (target!=='#') {
window.location.href=target;
return false;
}
}
}
if (canChangeState) {
selected = ar.toggle();
}
if (opts.boundList && opts.boundList.length > 0) {
m.setBoundListProperties(opts, list_target, ar.isSelected());
}
areaOpts = ar.effectiveOptions();
if (areaOpts.includeKeys) {
list = u.split(areaOpts.includeKeys);
$.each(list, function (i, e) {
var ar = me.getDataForKey(e.toString());
if (!ar.options.isMask) {
clickArea(ar);
}
});
}
}
mousedown.call(this,e);
if (opts.clickNavigate && ar.href) {
window.location.href=ar.href;
return;
}
if (ar && !ar.owner.currentAction) {
opts = me.options;
clickArea(ar);
}
}
/**
* Prototype for a MapData object, representing an ImageMapster bound object
* @param {Element} image an IMG element
* @param {object} options ImageMapster binding options
*/
m.MapData = function (image, options)
{
var me = this;
// (Image) main map image
me.image = image;
me.images = new m.MapImages(me);
me.graphics = new m.Graphics(me);
// save the initial style of the image for unbinding. This is problematic, chrome
// duplicates styles when assigning, and cssText is apparently not universally supported.
// Need to do something more robust to make unbinding work universally.
me.imgCssText = image.style.cssText || null;
initializeDefaults(me);
me.configureOptions(options);
// create context-bound event handlers from our private functions
me.mouseover = function(e) { mouseover.call(this,me,e); };
me.mouseout = function(e) { mouseout.call(this,me,e); };
me.click = function(e) { click.call(this,me,e); };
me.clearEffects = function(e) { clearEffects.call(this,me,e); };
};
m.MapData.prototype = {
constructor: m.MapData,
/**
* Set target.options from defaults + options
* @param {[type]} target The target
* @param {[type]} options The options to merge
*/
configureOptions: function(options) {
this.options= u.updateProps({}, m.defaults, options);
},
/**
* Ensure all images are loaded
* @return {Promise} A promise that resolves when the images have finished loading (or fail)
*/
bindImages: function() {
var me=this,
mi = me.images;
// reset the images if this is a rebind
if (mi.length>2) {
mi.splice(2);
} else if (mi.length===0) {
// add the actual main image
mi.add(me.image);
// will create a duplicate of the main image, we need this to get raw size info
mi.add(me.image.src);
}
configureAltImages(me);
return me.images.bind();
},
/**
* Test whether an async action is currently in progress
* @return {Boolean} true or false indicating state
*/
isActive: function() {
return !this.complete || this.currentAction;
},
/**
* Return an object indicating the various states. This isn't really used by
* production code.
*
* @return {object} An object with properties for various states
*/
state: function () {
return {
complete: this.complete,
resizing: this.currentAction==='resizing',
zoomed: this.zoomed,
zoomedArea: this.zoomedArea,
scaleInfo: this.scaleInfo
};
},
/**
* Get a unique ID for the wrapper of this imagemapster
* @return {string} A string that is unique to this image
*/
wrapId: function () {
return 'mapster_wrap_' + this.index;
},
_idFromKey: function (key) {
return typeof key === "string" && this._xref.hasOwnProperty(key) ?
this._xref[key] : -1;
},
/**
* Return a comma-separated string of all selected keys
* @return {string} CSV of all keys that are currently selected
*/
getSelected: function () {
var result = '';
$.each(this.data, function (i,e) {
if (e.isSelected()) {
result += (result ? ',' : '') + this.key;
}
});
return result;
},
/**
* Get an array of MapAreas associated with a specific AREA based on the keys for that area
* @param {Element} area An HTML AREA
* @param {number} atMost A number limiting the number of areas to be returned (typically 1 or 0 for no limit)
* @return {MapArea[]} Array of MapArea objects
*/
getAllDataForArea:function (area,atMost) {
var i,ar, result,
me=this,
key = $(area).filter('area').attr(me.options.mapKey);
if (key) {
result=[];
key = u.split(key);
for (i=0;i<(atMost || key.length);i++) {
ar = me.data[me._idFromKey(key[i])];
ar.area=area.length ? area[0]:area;
// set the actual area moused over/selected
// TODO: this is a brittle model for capturing which specific area - if this method was not used,
// ar.area could have old data. fix this.
result.push(ar);
}
}
return result;
},
getDataForArea: function(area) {
var ar=this.getAllDataForArea(area,1);
return ar ? ar[0] || null : null;
},
getDataForKey: function (key) {
return this.data[this._idFromKey(key)];
},
/**
* Get the primary keys associated with an area group.
* If this is a primary key, it will be returned.
*
* @param {string key An area key
* @return {string} A CSV of area keys
*/
getKeysForGroup: function(key) {
var ar=this.getDataForKey(key);
return !ar ? '':
ar.isPrimary ?
ar.key :
this.getPrimaryKeysForMapAreas(ar.areas()).join(',');
},
/**
* given an array of MapArea object, return an array of its unique primary keys
* @param {MapArea[]} areas The areas to analyze
* @return {string[]} An array of unique primary keys
*/
getPrimaryKeysForMapAreas: function(areas)
{
var keys=[];
$.each(areas,function(i,e) {
if ($.inArray(e.keys[0],keys)<0) {
keys.push(e.keys[0]);
}
});
return keys;
},
getData: function (obj) {
if (typeof obj === 'string') {
return this.getDataForKey(obj);
} else if (obj && obj.mapster || u.isElement(obj)) {
return this.getDataForArea(obj);
} else {
return null;
}
},
// remove highlight if present, raise event
ensureNoHighlight: function () {
var ar;
if (this.highlightId >= 0) {
this.graphics.clearHighlight();
ar = this.data[this.highlightId];
ar.changeState('highlight', false);
this.setHighlightId(-1);
}
},
setHighlightId: function(id) {
this.highlightId = id;
},
/**
* Clear all active selections on this map
*/
clearSelections: function () {
$.each(this.data, function (i,e) {
if (e.selected) {
e.deselect(true);
}
});
this.removeSelectionFinish();
},
/**
* Set area options from an array of option data.
*
* @param {object[]} areas An array of objects containing area-specific options
*/
setAreaOptions: function (areas) {
var i, area_options, ar;
areas = areas || [];
// refer by: map_data.options[map_data.data[x].area_option_id]
for (i = areas.length - 1; i >= 0; i--) {
area_options = areas[i];
if (area_options) {
ar = this.getDataForKey(area_options.key);
if (ar) {
u.updateProps(ar.options, area_options);
// TODO: will not deselect areas that were previously selected, so this only works
// for an initial bind.
if (u.isBool(area_options.selected)) {
ar.selected = area_options.selected;
}
}
}
}
},
// keys: a comma-separated list
drawSelections: function (keys) {
var i, key_arr = u.asArray(keys);
for (i = key_arr.length - 1; i >= 0; i--) {
this.data[key_arr[i]].drawSelection();
}
},
redrawSelections: function () {
$.each(this.data, function (i, e) {
if (e.isSelectedOrStatic()) {
e.drawSelection();
}
});
},
///called when images are done loading
initialize: function () {
var imgCopy, base_canvas, overlay_canvas, wrap, parentId, css, i,size,
img,sort_func, sorted_list, scale,
me = this,
opts = me.options;
if (me.complete) {
return;
}
img = $(me.image);
parentId = img.parent().attr('id');
// create a div wrapper only if there's not already a wrapper, otherwise, own it
if (parentId && parentId.length >= 12 && parentId.substring(0, 12) === "mapster_wrap") {
wrap = img.parent();
wrap.attr('id', me.wrapId());
} else {
wrap = $('