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