GPQAPP
2781 строка · 120.1 Кб
1/*!
2*
3* Jquery Mapael - Dynamic maps jQuery plugin (based on raphael.js)
4* Requires jQuery, raphael.js and jquery.mousewheel
5*
6* Version: 2.2.0
7*
8* Copyright (c) 2017 Vincent Brouté (https://www.vincentbroute.fr/mapael)
9* Licensed under the MIT license (http://www.opensource.org/licenses/mit-license.php).
10*
11* Thanks to Indigo744
12*
13*/
14(function (factory) {15if (typeof exports === 'object') {16// CommonJS17module.exports = factory(require('jquery'), require('raphael'), require('jquery-mousewheel'));18} else if (typeof define === 'function' && define.amd) {19// AMD. Register as an anonymous module.20define(['jquery', 'raphael', 'mousewheel'], factory);21} else {22// Browser globals23factory(jQuery, Raphael, jQuery.fn.mousewheel);24}25}(function ($, Raphael, mousewheel, undefined) {26
27"use strict";28
29// The plugin name (used on several places)30var pluginName = "mapael";31
32// Version number of jQuery Mapael. See http://semver.org/ for more information.33var version = "2.2.0";34
35/*36* Mapael constructor
37* Init instance vars and call init()
38* @param container the DOM element on which to apply the plugin
39* @param options the complete options to use
40*/
41var Mapael = function (container, options) {42var self = this;43
44// the global container (DOM element object)45self.container = container;46
47// the global container (jQuery object)48self.$container = $(container);49
50// the global options51self.options = self.extendDefaultOptions(options);52
53// zoom TimeOut handler (used to set and clear)54self.zoomTO = 0;55
56// zoom center coordinate (set at touchstart)57self.zoomCenterX = 0;58self.zoomCenterY = 0;59
60// Zoom pinch (set at touchstart and touchmove)61self.previousPinchDist = 0;62
63// Zoom data64self.zoomData = {65zoomLevel: 0,66zoomX: 0,67zoomY: 0,68panX: 0,69panY: 070};71
72self.currentViewBox = {73x: 0, y: 0, w: 0, h: 074};75
76// Panning: tell if panning action is in progress77self.panning = false;78
79// Animate view box80self.zoomAnimID = null; // Interval handler (used to set and clear)81self.zoomAnimStartTime = null; // Animation start time82self.zoomAnimCVBTarget = null; // Current ViewBox target83
84// Map subcontainer jQuery object85self.$map = $("." + self.options.map.cssClass, self.container);86
87// Save initial HTML content (used by destroy method)88self.initialMapHTMLContent = self.$map.html();89
90// The tooltip jQuery object91self.$tooltip = {};92
93// The paper Raphael object94self.paper = {};95
96// The areas object list97self.areas = {};98
99// The plots object list100self.plots = {};101
102// The links object list103self.links = {};104
105// The legends list106self.legends = {};107
108// The map configuration object (taken from map file)109self.mapConf = {};110
111// Holds all custom event handlers112self.customEventHandlers = {};113
114// Let's start the initialization115self.init();116};117
118/*119* Mapael Prototype
120* Defines all methods and properties needed by Mapael
121* Each mapael object inherits their properties and methods from this prototype
122*/
123Mapael.prototype = {124
125/* Filtering TimeOut value in ms126* Used for mouseover trigger over elements */
127MouseOverFilteringTO: 120,128/* Filtering TimeOut value in ms129* Used for afterPanning trigger when panning */
130panningFilteringTO: 150,131/* Filtering TimeOut value in ms132* Used for mouseup/touchend trigger when panning */
133panningEndFilteringTO: 50,134/* Filtering TimeOut value in ms135* Used for afterZoom trigger when zooming */
136zoomFilteringTO: 150,137/* Filtering TimeOut value in ms138* Used for when resizing window */
139resizeFilteringTO: 150,140
141/*142* Initialize the plugin
143* Called by the constructor
144*/
145init: function () {146var self = this;147
148// Init check for class existence149if (self.options.map.cssClass === "" || $("." + self.options.map.cssClass, self.container).length === 0) {150throw new Error("The map class `" + self.options.map.cssClass + "` doesn't exists");151}152
153// Create the tooltip container154self.$tooltip = $("<div>").addClass(self.options.map.tooltip.cssClass).css("display", "none");155
156// Get the map container, empty it then append tooltip157self.$map.empty().append(self.$tooltip);158
159// Get the map from $.mapael or $.fn.mapael (backward compatibility)160if ($[pluginName] && $[pluginName].maps && $[pluginName].maps[self.options.map.name]) {161// Mapael version >= 2.x162self.mapConf = $[pluginName].maps[self.options.map.name];163} else if ($.fn[pluginName] && $.fn[pluginName].maps && $.fn[pluginName].maps[self.options.map.name]) {164// Mapael version <= 1.x - DEPRECATED165self.mapConf = $.fn[pluginName].maps[self.options.map.name];166if (window.console && window.console.warn) {167window.console.warn("Extending $.fn.mapael is deprecated (map '" + self.options.map.name + "')");168}169} else {170throw new Error("Unknown map '" + self.options.map.name + "'");171}172
173// Create Raphael paper174self.paper = new Raphael(self.$map[0], self.mapConf.width, self.mapConf.height);175
176// issue #135: Check for Raphael bug on text element boundaries177if (self.isRaphaelBBoxBugPresent() === true) {178self.destroy();179throw new Error("Can't get boundary box for text (is your container hidden? See #135)");180}181
182// add plugin class name on element183self.$container.addClass(pluginName);184
185if (self.options.map.tooltip.css) self.$tooltip.css(self.options.map.tooltip.css);186self.setViewBox(0, 0, self.mapConf.width, self.mapConf.height);187
188// Handle map size189if (self.options.map.width) {190// NOT responsive: map has a fixed width191self.paper.setSize(self.options.map.width, self.mapConf.height * (self.options.map.width / self.mapConf.width));192} else {193// Responsive: handle resizing of the map194self.initResponsiveSize();195}196
197// Draw map areas198$.each(self.mapConf.elems, function (id) {199// Init area object200self.areas[id] = {};201// Set area options202self.areas[id].options = self.getElemOptions(203self.options.map.defaultArea,204(self.options.areas[id] ? self.options.areas[id] : {}),205self.options.legend.area206);207// draw area208self.areas[id].mapElem = self.paper.path(self.mapConf.elems[id]);209});210
211// Hook that allows to add custom processing on the map212if (self.options.map.beforeInit) self.options.map.beforeInit(self.$container, self.paper, self.options);213
214// Init map areas in a second loop215// Allows text to be added after ALL areas and prevent them from being hidden216$.each(self.mapConf.elems, function (id) {217self.initElem(id, 'area', self.areas[id]);218});219
220// Draw links221self.links = self.drawLinksCollection(self.options.links);222
223// Draw plots224$.each(self.options.plots, function (id) {225self.plots[id] = self.drawPlot(id);226});227
228// Attach zoom event229self.$container.on("zoom." + pluginName, function (e, zoomOptions) {230self.onZoomEvent(e, zoomOptions);231});232
233if (self.options.map.zoom.enabled) {234// Enable zoom235self.initZoom(self.mapConf.width, self.mapConf.height, self.options.map.zoom);236}237
238// Set initial zoom239if (self.options.map.zoom.init !== undefined) {240if (self.options.map.zoom.init.animDuration === undefined) {241self.options.map.zoom.init.animDuration = 0;242}243self.$container.trigger("zoom", self.options.map.zoom.init);244}245
246// Create the legends for areas247self.createLegends("area", self.areas, 1);248
249// Create the legends for plots taking into account the scale of the map250self.createLegends("plot", self.plots, self.paper.width / self.mapConf.width);251
252// Attach update event253self.$container.on("update." + pluginName, function (e, opt) {254self.onUpdateEvent(e, opt);255});256
257// Attach showElementsInRange event258self.$container.on("showElementsInRange." + pluginName, function (e, opt) {259self.onShowElementsInRange(e, opt);260});261
262// Attach delegated events263self.initDelegatedMapEvents();264// Attach delegated custom events265self.initDelegatedCustomEvents();266
267// Hook that allows to add custom processing on the map268if (self.options.map.afterInit) self.options.map.afterInit(self.$container, self.paper, self.areas, self.plots, self.options);269
270$(self.paper.desc).append(" and Mapael " + self.version + " (https://www.vincentbroute.fr/mapael/)");271},272
273/*274* Destroy mapael
275* This function effectively detach mapael from the container
276* - Set the container back to the way it was before mapael instanciation
277* - Remove all data associated to it (memory can then be free'ed by browser)
278*
279* This method can be call directly by user:
280* $(".mapcontainer").data("mapael").destroy();
281*
282* This method is also automatically called if the user try to call mapael
283* on a container already containing a mapael instance
284*/
285destroy: function () {286var self = this;287
288// Detach all event listeners attached to the container289self.$container.off("." + pluginName);290self.$map.off("." + pluginName);291
292// Detach the global resize event handler293if (self.onResizeEvent) $(window).off("resize." + pluginName, self.onResizeEvent);294
295// Empty the container (this will also detach all event listeners)296self.$map.empty();297
298// Replace initial HTML content299self.$map.html(self.initialMapHTMLContent);300
301// Empty legend containers and replace initial HTML content302$.each(self.legends, function(legendType) {303$.each(self.legends[legendType], function(legendIndex) {304var legend = self.legends[legendType][legendIndex];305legend.container.empty();306legend.container.html(legend.initialHTMLContent);307});308});309
310// Remove mapael class311self.$container.removeClass(pluginName);312
313// Remove the data314self.$container.removeData(pluginName);315
316// Remove all internal reference317self.container = undefined;318self.$container = undefined;319self.options = undefined;320self.paper = undefined;321self.$map = undefined;322self.$tooltip = undefined;323self.mapConf = undefined;324self.areas = undefined;325self.plots = undefined;326self.links = undefined;327self.customEventHandlers = undefined;328},329
330initResponsiveSize: function () {331var self = this;332var resizeTO = null;333
334// Function that actually handle the resizing335var handleResize = function(isInit) {336var containerWidth = self.$map.width();337
338if (self.paper.width !== containerWidth) {339var newScale = containerWidth / self.mapConf.width;340// Set new size341self.paper.setSize(containerWidth, self.mapConf.height * newScale);342
343// Create plots legend again to take into account the new scale344// Do not do this on init (it will be done later)345if (isInit !== true && self.options.legend.redrawOnResize) {346self.createLegends("plot", self.plots, newScale);347}348}349};350
351self.onResizeEvent = function() {352// Clear any previous setTimeout (avoid too much triggering)353clearTimeout(resizeTO);354// setTimeout to wait for the user to finish its resizing355resizeTO = setTimeout(function () {356handleResize();357}, self.resizeFilteringTO);358};359
360// Attach resize handler361$(window).on("resize." + pluginName, self.onResizeEvent);362
363// Call once364handleResize(true);365},366
367/*368* Extend the user option with the default one
369* @param options the user options
370* @return new options object
371*/
372extendDefaultOptions: function (options) {373
374// Extend default options with user options375options = $.extend(true, {}, Mapael.prototype.defaultOptions, options);376
377// Extend legend default options378$.each(['area', 'plot'], function (key, type) {379if ($.isArray(options.legend[type])) {380for (var i = 0; i < options.legend[type].length; ++i)381options.legend[type][i] = $.extend(true, {}, Mapael.prototype.legendDefaultOptions[type], options.legend[type][i]);382} else {383options.legend[type] = $.extend(true, {}, Mapael.prototype.legendDefaultOptions[type], options.legend[type]);384}385});386
387return options;388},389
390/*391* Init all delegated events for the whole map:
392* mouseover
393* mousemove
394* mouseout
395*/
396initDelegatedMapEvents: function() {397var self = this;398
399// Mapping between data-type value and the corresponding elements array400// Note: legend-elem and legend-label are not in this table because401// they need a special processing402var dataTypeToElementMapping = {403'area' : self.areas,404'area-text' : self.areas,405'plot' : self.plots,406'plot-text' : self.plots,407'link' : self.links,408'link-text' : self.links409};410
411/* Attach mouseover event delegation412* Note: we filter the event with a timeout to reduce the firing when the mouse moves quickly
413*/
414var mapMouseOverTimeoutID;415self.$container.on("mouseover." + pluginName, "[data-id]", function () {416var elem = this;417clearTimeout(mapMouseOverTimeoutID);418mapMouseOverTimeoutID = setTimeout(function() {419var $elem = $(elem);420var id = $elem.attr('data-id');421var type = $elem.attr('data-type');422
423if (dataTypeToElementMapping[type] !== undefined) {424self.elemEnter(dataTypeToElementMapping[type][id]);425} else if (type === 'legend-elem' || type === 'legend-label') {426var legendIndex = $elem.attr('data-legend-id');427var legendType = $elem.attr('data-legend-type');428self.elemEnter(self.legends[legendType][legendIndex].elems[id]);429}430}, self.MouseOverFilteringTO);431});432
433/* Attach mousemove event delegation434* Note: timeout filtering is small to update the Tooltip position fast
435*/
436var mapMouseMoveTimeoutID;437self.$container.on("mousemove." + pluginName, "[data-id]", function (event) {438var elem = this;439clearTimeout(mapMouseMoveTimeoutID);440mapMouseMoveTimeoutID = setTimeout(function() {441var $elem = $(elem);442var id = $elem.attr('data-id');443var type = $elem.attr('data-type');444
445if (dataTypeToElementMapping[type] !== undefined) {446self.elemHover(dataTypeToElementMapping[type][id], event);447} else if (type === 'legend-elem' || type === 'legend-label') {448/* Nothing to do */449}450
451}, 0);452});453
454/* Attach mouseout event delegation455* Note: we don't perform any timeout filtering to clear & reset elem ASAP
456* Otherwise an element may be stuck in 'hover' state (which is NOT good)
457*/
458self.$container.on("mouseout." + pluginName, "[data-id]", function () {459var elem = this;460// Clear any461clearTimeout(mapMouseOverTimeoutID);462clearTimeout(mapMouseMoveTimeoutID);463var $elem = $(elem);464var id = $elem.attr('data-id');465var type = $elem.attr('data-type');466
467if (dataTypeToElementMapping[type] !== undefined) {468self.elemOut(dataTypeToElementMapping[type][id]);469} else if (type === 'legend-elem' || type === 'legend-label') {470var legendIndex = $elem.attr('data-legend-id');471var legendType = $elem.attr('data-legend-type');472self.elemOut(self.legends[legendType][legendIndex].elems[id]);473}474});475
476/* Attach click event delegation477* Note: we filter the event with a timeout to avoid double click
478*/
479self.$container.on("click." + pluginName, "[data-id]", function (evt, opts) {480var $elem = $(this);481var id = $elem.attr('data-id');482var type = $elem.attr('data-type');483
484if (dataTypeToElementMapping[type] !== undefined) {485self.elemClick(dataTypeToElementMapping[type][id]);486} else if (type === 'legend-elem' || type === 'legend-label') {487var legendIndex = $elem.attr('data-legend-id');488var legendType = $elem.attr('data-legend-type');489self.handleClickOnLegendElem(self.legends[legendType][legendIndex].elems[id], id, legendIndex, legendType, opts);490}491});492},493
494/*495* Init all delegated custom events
496*/
497initDelegatedCustomEvents: function() {498var self = this;499
500$.each(self.customEventHandlers, function(eventName) {501// Namespace the custom event502// This allow to easily unbound only custom events and not regular ones503var fullEventName = eventName + '.' + pluginName + ".custom";504self.$container.off(fullEventName).on(fullEventName, "[data-id]", function (e) {505var $elem = $(this);506var id = $elem.attr('data-id');507var type = $elem.attr('data-type').replace('-text', '');508
509if (!self.panning &&510self.customEventHandlers[eventName][type] !== undefined &&511self.customEventHandlers[eventName][type][id] !== undefined)512{513// Get back related elem514var elem = self.customEventHandlers[eventName][type][id];515// Run callback provided by user516elem.options.eventHandlers[eventName](e, id, elem.mapElem, elem.textElem, elem.options);517}518});519});520
521},522
523/*524* Init the element "elem" on the map (drawing text, setting attributes, events, tooltip, ...)
525*
526* @param id the id of the element
527* @param type the type of the element (area, plot, link)
528* @param elem object the element object (with mapElem), it will be updated
529*/
530initElem: function (id, type, elem) {531var self = this;532var $mapElem = $(elem.mapElem.node);533
534// If an HTML link exists for this element, add cursor attributes535if (elem.options.href) {536elem.options.attrs.cursor = "pointer";537if (elem.options.text) elem.options.text.attrs.cursor = "pointer";538}539
540// Set SVG attributes to map element541elem.mapElem.attr(elem.options.attrs);542// Set DOM attributes to map element543$mapElem.attr({544"data-id": id,545"data-type": type546});547if (elem.options.cssClass !== undefined) {548$mapElem.addClass(elem.options.cssClass);549}550
551// Init the label related to the element552if (elem.options.text && elem.options.text.content !== undefined) {553// Set a text label in the area554var textPosition = self.getTextPosition(elem.mapElem.getBBox(), elem.options.text.position, elem.options.text.margin);555elem.options.text.attrs.text = elem.options.text.content;556elem.options.text.attrs.x = textPosition.x;557elem.options.text.attrs.y = textPosition.y;558elem.options.text.attrs['text-anchor'] = textPosition.textAnchor;559// Draw text560elem.textElem = self.paper.text(textPosition.x, textPosition.y, elem.options.text.content);561// Apply SVG attributes to text element562elem.textElem.attr(elem.options.text.attrs);563// Apply DOM attributes564$(elem.textElem.node).attr({565"data-id": id,566"data-type": type + '-text'567});568}569
570// Set user event handlers571if (elem.options.eventHandlers) self.setEventHandlers(id, type, elem);572
573// Set hover option for mapElem574self.setHoverOptions(elem.mapElem, elem.options.attrs, elem.options.attrsHover);575
576// Set hover option for textElem577if (elem.textElem) self.setHoverOptions(elem.textElem, elem.options.text.attrs, elem.options.text.attrsHover);578},579
580/*581* Init zoom and panning for the map
582* @param mapWidth
583* @param mapHeight
584* @param zoomOptions
585*/
586initZoom: function (mapWidth, mapHeight, zoomOptions) {587var self = this;588var mousedown = false;589var previousX = 0;590var previousY = 0;591var fnZoomButtons = {592"reset": function () {593self.$container.trigger("zoom", {"level": 0});594},595"in": function () {596self.$container.trigger("zoom", {"level": "+1"});597},598"out": function () {599self.$container.trigger("zoom", {"level": -1});600}601};602
603// init Zoom data604$.extend(self.zoomData, {605zoomLevel: 0,606panX: 0,607panY: 0608});609
610// init zoom buttons611$.each(zoomOptions.buttons, function(type, opt) {612if (fnZoomButtons[type] === undefined) throw new Error("Unknown zoom button '" + type + "'");613// Create div with classes, contents and title (for tooltip)614var $button = $("<div>").addClass(opt.cssClass)615.html(opt.content)616.attr("title", opt.title);617// Assign click event618$button.on("click." + pluginName, fnZoomButtons[type]);619// Append to map620self.$map.append($button);621});622
623// Update the zoom level of the map on mousewheel624if (self.options.map.zoom.mousewheel) {625self.$map.on("mousewheel." + pluginName, function (e) {626var zoomLevel = (e.deltaY > 0) ? 1 : -1;627var coord = self.mapPagePositionToXY(e.pageX, e.pageY);628
629self.$container.trigger("zoom", {630"fixedCenter": true,631"level": self.zoomData.zoomLevel + zoomLevel,632"x": coord.x,633"y": coord.y634});635
636e.preventDefault();637});638}639
640// Update the zoom level of the map on touch pinch641if (self.options.map.zoom.touch) {642self.$map.on("touchstart." + pluginName, function (e) {643if (e.originalEvent.touches.length === 2) {644self.zoomCenterX = (e.originalEvent.touches[0].pageX + e.originalEvent.touches[1].pageX) / 2;645self.zoomCenterY = (e.originalEvent.touches[0].pageY + e.originalEvent.touches[1].pageY) / 2;646self.previousPinchDist = Math.sqrt(Math.pow((e.originalEvent.touches[1].pageX - e.originalEvent.touches[0].pageX), 2) + Math.pow((e.originalEvent.touches[1].pageY - e.originalEvent.touches[0].pageY), 2));647}648});649
650self.$map.on("touchmove." + pluginName, function (e) {651var pinchDist = 0;652var zoomLevel = 0;653
654if (e.originalEvent.touches.length === 2) {655pinchDist = Math.sqrt(Math.pow((e.originalEvent.touches[1].pageX - e.originalEvent.touches[0].pageX), 2) + Math.pow((e.originalEvent.touches[1].pageY - e.originalEvent.touches[0].pageY), 2));656
657if (Math.abs(pinchDist - self.previousPinchDist) > 15) {658var coord = self.mapPagePositionToXY(self.zoomCenterX, self.zoomCenterY);659zoomLevel = (pinchDist - self.previousPinchDist) / Math.abs(pinchDist - self.previousPinchDist);660self.$container.trigger("zoom", {661"fixedCenter": true,662"level": self.zoomData.zoomLevel + zoomLevel,663"x": coord.x,664"y": coord.y665});666self.previousPinchDist = pinchDist;667}668return false;669}670});671}672
673// When the user drag the map, prevent to move the clicked element instead of dragging the map (behaviour seen with Firefox)674self.$map.on("dragstart", function() {675return false;676});677
678// Panning679var panningMouseUpTO = null;680var panningMouseMoveTO = null;681$("body").on("mouseup." + pluginName + (zoomOptions.touch ? " touchend." + pluginName : ""), function () {682mousedown = false;683clearTimeout(panningMouseUpTO);684clearTimeout(panningMouseMoveTO);685panningMouseUpTO = setTimeout(function () {686self.panning = false;687}, self.panningEndFilteringTO);688});689
690self.$map.on("mousedown." + pluginName + (zoomOptions.touch ? " touchstart." + pluginName : ""), function (e) {691clearTimeout(panningMouseUpTO);692clearTimeout(panningMouseMoveTO);693if (e.pageX !== undefined) {694mousedown = true;695previousX = e.pageX;696previousY = e.pageY;697} else {698if (e.originalEvent.touches.length === 1) {699mousedown = true;700previousX = e.originalEvent.touches[0].pageX;701previousY = e.originalEvent.touches[0].pageY;702}703}704}).on("mousemove." + pluginName + (zoomOptions.touch ? " touchmove." + pluginName : ""), function (e) {705var currentLevel = self.zoomData.zoomLevel;706var pageX = 0;707var pageY = 0;708
709clearTimeout(panningMouseUpTO);710clearTimeout(panningMouseMoveTO);711
712if (e.pageX !== undefined) {713pageX = e.pageX;714pageY = e.pageY;715} else {716if (e.originalEvent.touches.length === 1) {717pageX = e.originalEvent.touches[0].pageX;718pageY = e.originalEvent.touches[0].pageY;719} else {720mousedown = false;721}722}723
724if (mousedown && currentLevel !== 0) {725var offsetX = (previousX - pageX) / (1 + (currentLevel * zoomOptions.step)) * (mapWidth / self.paper.width);726var offsetY = (previousY - pageY) / (1 + (currentLevel * zoomOptions.step)) * (mapHeight / self.paper.height);727var panX = Math.min(Math.max(0, self.currentViewBox.x + offsetX), (mapWidth - self.currentViewBox.w));728var panY = Math.min(Math.max(0, self.currentViewBox.y + offsetY), (mapHeight - self.currentViewBox.h));729
730if (Math.abs(offsetX) > 5 || Math.abs(offsetY) > 5) {731$.extend(self.zoomData, {732panX: panX,733panY: panY,734zoomX: panX + self.currentViewBox.w / 2,735zoomY: panY + self.currentViewBox.h / 2736});737self.setViewBox(panX, panY, self.currentViewBox.w, self.currentViewBox.h);738
739panningMouseMoveTO = setTimeout(function () {740self.$map.trigger("afterPanning", {741x1: panX,742y1: panY,743x2: (panX + self.currentViewBox.w),744y2: (panY + self.currentViewBox.h)745});746}, self.panningFilteringTO);747
748previousX = pageX;749previousY = pageY;750self.panning = true;751}752return false;753}754});755},756
757/*758* Map a mouse position to a map position
759* Transformation principle:
760* ** start with (pageX, pageY) absolute mouse coordinate
761* - Apply translation: take into accounts the map offset in the page
762* ** from this point, we have relative mouse coordinate
763* - Apply homothetic transformation: take into accounts initial factor of map sizing (fullWidth / actualWidth)
764* - Apply homothetic transformation: take into accounts the zoom factor
765* ** from this point, we have relative map coordinate
766* - Apply translation: take into accounts the current panning of the map
767* ** from this point, we have absolute map coordinate
768* @param pageX: mouse client coordinate on X
769* @param pageY: mouse client coordinate on Y
770* @return map coordinate {x, y}
771*/
772mapPagePositionToXY: function(pageX, pageY) {773var self = this;774var offset = self.$map.offset();775var initFactor = (self.options.map.width) ? (self.mapConf.width / self.options.map.width) : (self.mapConf.width / self.$map.width());776var zoomFactor = 1 / (1 + (self.zoomData.zoomLevel * self.options.map.zoom.step));777return {778x: (zoomFactor * initFactor * (pageX - offset.left)) + self.zoomData.panX,779y: (zoomFactor * initFactor * (pageY - offset.top)) + self.zoomData.panY780};781},782
783/*784* Zoom on the map
785*
786* zoomOptions.animDuration zoom duration
787*
788* zoomOptions.level level of the zoom between minLevel and maxLevel (absolute number, or relative string +1 or -1)
789* zoomOptions.fixedCenter set to true in order to preserve the position of x,y in the canvas when zoomed
790*
791* zoomOptions.x x coordinate of the point to focus on
792* zoomOptions.y y coordinate of the point to focus on
793* - OR -
794* zoomOptions.latitude latitude of the point to focus on
795* zoomOptions.longitude longitude of the point to focus on
796* - OR -
797* zoomOptions.plot plot ID to focus on
798* - OR -
799* zoomOptions.area area ID to focus on
800* zoomOptions.areaMargin margin (in pixels) around the area
801*
802* If an area ID is specified, the algorithm will override the zoom level to focus on the area
803* but it may be limited by the min/max zoom level limits set at initialization.
804*
805* If no coordinates are specified, the zoom will be focused on the center of the current view box
806*
807*/
808onZoomEvent: function (e, zoomOptions) {809var self = this;810
811// new Top/Left corner coordinates812var panX;813var panY;814// new Width/Height viewbox size815var panWidth;816var panHeight;817
818// Zoom level in absolute scale (from 0 to max, by step of 1)819var zoomLevel = self.zoomData.zoomLevel;820
821// Relative zoom level (from 1 to max, by step of 0.25 (default))822var previousRelativeZoomLevel = 1 + self.zoomData.zoomLevel * self.options.map.zoom.step;823var relativeZoomLevel;824
825var animDuration = (zoomOptions.animDuration !== undefined) ? zoomOptions.animDuration : self.options.map.zoom.animDuration;826
827if (zoomOptions.area !== undefined) {828/* An area is given829* We will define x/y coordinate AND a new zoom level to fill the area
830*/
831if (self.areas[zoomOptions.area] === undefined) throw new Error("Unknown area '" + zoomOptions.area + "'");832var areaMargin = (zoomOptions.areaMargin !== undefined) ? zoomOptions.areaMargin : 10;833var areaBBox = self.areas[zoomOptions.area].mapElem.getBBox();834var areaFullWidth = areaBBox.width + 2 * areaMargin;835var areaFullHeight = areaBBox.height + 2 * areaMargin;836
837// Compute new x/y focus point (center of area)838zoomOptions.x = areaBBox.cx;839zoomOptions.y = areaBBox.cy;840
841// Compute a new absolute zoomLevel value (inverse of relative -> absolute)842// Take the min between zoomLevel on width vs. height to be able to see the whole area843zoomLevel = Math.min(Math.floor((self.mapConf.width / areaFullWidth - 1) / self.options.map.zoom.step),844Math.floor((self.mapConf.height / areaFullHeight - 1) / self.options.map.zoom.step));845
846} else {847
848// Get user defined zoom level849if (zoomOptions.level !== undefined) {850if (typeof zoomOptions.level === "string") {851// level is a string, either "n", "+n" or "-n"852if ((zoomOptions.level.slice(0, 1) === '+') || (zoomOptions.level.slice(0, 1) === '-')) {853// zoomLevel is relative854zoomLevel = self.zoomData.zoomLevel + parseInt(zoomOptions.level, 10);855} else {856// zoomLevel is absolute857zoomLevel = parseInt(zoomOptions.level, 10);858}859} else {860// level is integer861if (zoomOptions.level < 0) {862// zoomLevel is relative863zoomLevel = self.zoomData.zoomLevel + zoomOptions.level;864} else {865// zoomLevel is absolute866zoomLevel = zoomOptions.level;867}868}869}870
871if (zoomOptions.plot !== undefined) {872if (self.plots[zoomOptions.plot] === undefined) throw new Error("Unknown plot '" + zoomOptions.plot + "'");873
874zoomOptions.x = self.plots[zoomOptions.plot].coords.x;875zoomOptions.y = self.plots[zoomOptions.plot].coords.y;876} else {877if (zoomOptions.latitude !== undefined && zoomOptions.longitude !== undefined) {878var coords = self.mapConf.getCoords(zoomOptions.latitude, zoomOptions.longitude);879zoomOptions.x = coords.x;880zoomOptions.y = coords.y;881}882
883if (zoomOptions.x === undefined) {884zoomOptions.x = self.currentViewBox.x + self.currentViewBox.w / 2;885}886
887if (zoomOptions.y === undefined) {888zoomOptions.y = self.currentViewBox.y + self.currentViewBox.h / 2;889}890}891}892
893// Make sure we stay in the zoom level boundaries894zoomLevel = Math.min(Math.max(zoomLevel, self.options.map.zoom.minLevel), self.options.map.zoom.maxLevel);895
896// Compute relative zoom level897relativeZoomLevel = 1 + zoomLevel * self.options.map.zoom.step;898
899// Compute panWidth / panHeight900panWidth = self.mapConf.width / relativeZoomLevel;901panHeight = self.mapConf.height / relativeZoomLevel;902
903if (zoomLevel === 0) {904panX = 0;905panY = 0;906} else {907if (zoomOptions.fixedCenter !== undefined && zoomOptions.fixedCenter === true) {908panX = self.zoomData.panX + ((zoomOptions.x - self.zoomData.panX) * (relativeZoomLevel - previousRelativeZoomLevel)) / relativeZoomLevel;909panY = self.zoomData.panY + ((zoomOptions.y - self.zoomData.panY) * (relativeZoomLevel - previousRelativeZoomLevel)) / relativeZoomLevel;910} else {911panX = zoomOptions.x - panWidth / 2;912panY = zoomOptions.y - panHeight / 2;913}914
915// Make sure we stay in the map boundaries916panX = Math.min(Math.max(0, panX), self.mapConf.width - panWidth);917panY = Math.min(Math.max(0, panY), self.mapConf.height - panHeight);918}919
920// Update zoom level of the map921if (relativeZoomLevel === previousRelativeZoomLevel && panX === self.zoomData.panX && panY === self.zoomData.panY) return;922
923if (animDuration > 0) {924self.animateViewBox(panX, panY, panWidth, panHeight, animDuration, self.options.map.zoom.animEasing);925} else {926self.setViewBox(panX, panY, panWidth, panHeight);927clearTimeout(self.zoomTO);928self.zoomTO = setTimeout(function () {929self.$map.trigger("afterZoom", {930x1: panX,931y1: panY,932x2: panX + panWidth,933y2: panY + panHeight934});935}, self.zoomFilteringTO);936}937
938$.extend(self.zoomData, {939zoomLevel: zoomLevel,940panX: panX,941panY: panY,942zoomX: panX + panWidth / 2,943zoomY: panY + panHeight / 2944});945},946
947/*948* Show some element in range defined by user
949* Triggered by user $(".mapcontainer").trigger("showElementsInRange", [opt]);
950*
951* @param opt the options
952* opt.hiddenOpacity opacity for hidden element (default = 0.3)
953* opt.animDuration animation duration in ms (default = 0)
954* opt.afterShowRange callback
955* opt.ranges the range to show:
956* Example:
957* opt.ranges = {
958* 'plot' : {
959* 0 : { // valueIndex
960* 'min': 1000,
961* 'max': 1200
962* },
963* 1 : { // valueIndex
964* 'min': 10,
965* 'max': 12
966* }
967* },
968* 'area' : {
969* {'min': 10, 'max': 20} // No valueIndex, only an object, use 0 as valueIndex (easy case)
970* }
971* }
972*/
973onShowElementsInRange: function(e, opt) {974var self = this;975
976// set animDuration to default if not defined977if (opt.animDuration === undefined) {978opt.animDuration = 0;979}980
981// set hiddenOpacity to default if not defined982if (opt.hiddenOpacity === undefined) {983opt.hiddenOpacity = 0.3;984}985
986// handle area987if (opt.ranges && opt.ranges.area) {988self.showElemByRange(opt.ranges.area, self.areas, opt.hiddenOpacity, opt.animDuration);989}990
991// handle plot992if (opt.ranges && opt.ranges.plot) {993self.showElemByRange(opt.ranges.plot, self.plots, opt.hiddenOpacity, opt.animDuration);994}995
996// handle link997if (opt.ranges && opt.ranges.link) {998self.showElemByRange(opt.ranges.link, self.links, opt.hiddenOpacity, opt.animDuration);999}1000
1001// Call user callback1002if (opt.afterShowRange) opt.afterShowRange();1003},1004
1005/*1006* Show some element in range
1007* @param ranges: the ranges
1008* @param elems: list of element on which to check against previous range
1009* @hiddenOpacity: the opacity when hidden
1010* @animDuration: the animation duration
1011*/
1012showElemByRange: function(ranges, elems, hiddenOpacity, animDuration) {1013var self = this;1014// Hold the final opacity value for all elements consolidated after applying each ranges1015// This allow to set the opacity only once for each elements1016var elemsFinalOpacity = {};1017
1018// set object with one valueIndex to 0 if we have directly the min/max1019if (ranges.min !== undefined || ranges.max !== undefined) {1020ranges = {0: ranges};1021}1022
1023// Loop through each valueIndex1024$.each(ranges, function (valueIndex) {1025var range = ranges[valueIndex];1026// Check if user defined at least a min or max value1027if (range.min === undefined && range.max === undefined) {1028return true; // skip this iteration (each loop), goto next range1029}1030// Loop through each elements1031$.each(elems, function (id) {1032var elemValue = elems[id].options.value;1033// set value with one valueIndex to 0 if not object1034if (typeof elemValue !== "object") {1035elemValue = [elemValue];1036}1037// Check existence of this value index1038if (elemValue[valueIndex] === undefined) {1039return true; // skip this iteration (each loop), goto next element1040}1041// Check if in range1042if ((range.min !== undefined && elemValue[valueIndex] < range.min) ||1043(range.max !== undefined && elemValue[valueIndex] > range.max)) {1044// Element not in range1045elemsFinalOpacity[id] = hiddenOpacity;1046} else {1047// Element in range1048elemsFinalOpacity[id] = 1;1049}1050});1051});1052// Now that we looped through all ranges, we can really assign the final opacity1053$.each(elemsFinalOpacity, function (id) {1054self.setElementOpacity(elems[id], elemsFinalOpacity[id], animDuration);1055});1056},1057
1058/*1059* Set element opacity
1060* Handle elem.mapElem and elem.textElem
1061* @param elem the element
1062* @param opacity the opacity to apply
1063* @param animDuration the animation duration to use
1064*/
1065setElementOpacity: function(elem, opacity, animDuration) {1066var self = this;1067
1068// Ensure no animation is running1069//elem.mapElem.stop();1070//if (elem.textElem) elem.textElem.stop();1071
1072// If final opacity is not null, ensure element is shown before proceeding1073if (opacity > 0) {1074elem.mapElem.show();1075if (elem.textElem) elem.textElem.show();1076}1077
1078self.animate(elem.mapElem, {"opacity": opacity}, animDuration, function () {1079// If final attribute is 0, hide1080if (opacity === 0) elem.mapElem.hide();1081});1082
1083self.animate(elem.textElem, {"opacity": opacity}, animDuration, function () {1084// If final attribute is 0, hide1085if (opacity === 0) elem.textElem.hide();1086});1087},1088
1089/*1090* Update the current map
1091*
1092* Refresh attributes and tooltips for areas and plots
1093* @param opt option for the refresh :
1094* opt.mapOptions: options to update for plots and areas
1095* opt.replaceOptions: whether mapsOptions should entirely replace current map options, or just extend it
1096* opt.opt.newPlots new plots to add to the map
1097* opt.newLinks new links to add to the map
1098* opt.deletePlotKeys plots to delete from the map (array, or "all" to remove all plots)
1099* opt.deleteLinkKeys links to remove from the map (array, or "all" to remove all links)
1100* opt.setLegendElemsState the state of legend elements to be set : show (default) or hide
1101* opt.animDuration animation duration in ms (default = 0)
1102* opt.afterUpdate hook that allows to add custom processing on the map
1103*/
1104onUpdateEvent: function (e, opt) {1105var self = this;1106// Abort if opt is undefined1107if (typeof opt !== "object") return;1108
1109var i = 0;1110var animDuration = (opt.animDuration) ? opt.animDuration : 0;1111
1112// This function remove an element using animation (or not, depending on animDuration)1113// Used for deletePlotKeys and deleteLinkKeys1114var fnRemoveElement = function (elem) {1115
1116self.animate(elem.mapElem, {"opacity": 0}, animDuration, function () {1117elem.mapElem.remove();1118});1119
1120self.animate(elem.textElem, {"opacity": 0}, animDuration, function () {1121elem.textElem.remove();1122});1123};1124
1125// This function show an element using animation1126// Used for newPlots and newLinks1127var fnShowElement = function (elem) {1128// Starts with hidden elements1129elem.mapElem.attr({opacity: 0});1130if (elem.textElem) elem.textElem.attr({opacity: 0});1131// Set final element opacity1132self.setElementOpacity(1133elem,1134(elem.mapElem.originalAttrs.opacity !== undefined) ? elem.mapElem.originalAttrs.opacity : 1,1135animDuration
1136);1137};1138
1139if (typeof opt.mapOptions === "object") {1140if (opt.replaceOptions === true) self.options = self.extendDefaultOptions(opt.mapOptions);1141else $.extend(true, self.options, opt.mapOptions);1142
1143// IF we update areas, plots or legend, then reset all legend state to "show"1144if (opt.mapOptions.areas !== undefined || opt.mapOptions.plots !== undefined || opt.mapOptions.legend !== undefined) {1145$("[data-type='legend-elem']", self.$container).each(function (id, elem) {1146if ($(elem).attr('data-hidden') === "1") {1147// Toggle state of element by clicking1148$(elem).trigger("click", {hideOtherElems: false, animDuration: animDuration});1149}1150});1151}1152}1153
1154// Delete plots by name if deletePlotKeys is array1155if (typeof opt.deletePlotKeys === "object") {1156for (; i < opt.deletePlotKeys.length; i++) {1157if (self.plots[opt.deletePlotKeys[i]] !== undefined) {1158fnRemoveElement(self.plots[opt.deletePlotKeys[i]]);1159delete self.plots[opt.deletePlotKeys[i]];1160}1161}1162// Delete ALL plots if deletePlotKeys is set to "all"1163} else if (opt.deletePlotKeys === "all") {1164$.each(self.plots, function (id, elem) {1165fnRemoveElement(elem);1166});1167// Empty plots object1168self.plots = {};1169}1170
1171// Delete links by name if deleteLinkKeys is array1172if (typeof opt.deleteLinkKeys === "object") {1173for (i = 0; i < opt.deleteLinkKeys.length; i++) {1174if (self.links[opt.deleteLinkKeys[i]] !== undefined) {1175fnRemoveElement(self.links[opt.deleteLinkKeys[i]]);1176delete self.links[opt.deleteLinkKeys[i]];1177}1178}1179// Delete ALL links if deleteLinkKeys is set to "all"1180} else if (opt.deleteLinkKeys === "all") {1181$.each(self.links, function (id, elem) {1182fnRemoveElement(elem);1183});1184// Empty links object1185self.links = {};1186}1187
1188// New plots1189if (typeof opt.newPlots === "object") {1190$.each(opt.newPlots, function (id) {1191if (self.plots[id] === undefined) {1192self.options.plots[id] = opt.newPlots[id];1193self.plots[id] = self.drawPlot(id);1194if (animDuration > 0) {1195fnShowElement(self.plots[id]);1196}1197}1198});1199}1200
1201// New links1202if (typeof opt.newLinks === "object") {1203var newLinks = self.drawLinksCollection(opt.newLinks);1204$.extend(self.links, newLinks);1205$.extend(self.options.links, opt.newLinks);1206if (animDuration > 0) {1207$.each(newLinks, function (id) {1208fnShowElement(newLinks[id]);1209});1210}1211}1212
1213// Update areas attributes and tooltips1214$.each(self.areas, function (id) {1215// Avoid updating unchanged elements1216if ((typeof opt.mapOptions === "object" &&1217(1218(typeof opt.mapOptions.map === "object" && typeof opt.mapOptions.map.defaultArea === "object") ||1219(typeof opt.mapOptions.areas === "object" && typeof opt.mapOptions.areas[id] === "object") ||1220(typeof opt.mapOptions.legend === "object" && typeof opt.mapOptions.legend.area === "object")1221)) || opt.replaceOptions === true1222) {1223self.areas[id].options = self.getElemOptions(1224self.options.map.defaultArea,1225(self.options.areas[id] ? self.options.areas[id] : {}),1226self.options.legend.area1227);1228self.updateElem(self.areas[id], animDuration);1229}1230});1231
1232// Update plots attributes and tooltips1233$.each(self.plots, function (id) {1234// Avoid updating unchanged elements1235if ((typeof opt.mapOptions ==="object" &&1236(1237(typeof opt.mapOptions.map === "object" && typeof opt.mapOptions.map.defaultPlot === "object") ||1238(typeof opt.mapOptions.plots === "object" && typeof opt.mapOptions.plots[id] === "object") ||1239(typeof opt.mapOptions.legend === "object" && typeof opt.mapOptions.legend.plot === "object")1240)) || opt.replaceOptions === true1241) {1242self.plots[id].options = self.getElemOptions(1243self.options.map.defaultPlot,1244(self.options.plots[id] ? self.options.plots[id] : {}),1245self.options.legend.plot1246);1247
1248self.setPlotCoords(self.plots[id]);1249self.setPlotAttributes(self.plots[id]);1250
1251self.updateElem(self.plots[id], animDuration);1252}1253});1254
1255// Update links attributes and tooltips1256$.each(self.links, function (id) {1257// Avoid updating unchanged elements1258if ((typeof opt.mapOptions === "object" &&1259(1260(typeof opt.mapOptions.map === "object" && typeof opt.mapOptions.map.defaultLink === "object") ||1261(typeof opt.mapOptions.links === "object" && typeof opt.mapOptions.links[id] === "object")1262)) || opt.replaceOptions === true1263) {1264self.links[id].options = self.getElemOptions(1265self.options.map.defaultLink,1266(self.options.links[id] ? self.options.links[id] : {}),1267{}1268);1269
1270self.updateElem(self.links[id], animDuration);1271}1272});1273
1274// Update legends1275if (opt.mapOptions && (1276(typeof opt.mapOptions.legend === "object") ||1277(typeof opt.mapOptions.map === "object" && typeof opt.mapOptions.map.defaultArea === "object") ||1278(typeof opt.mapOptions.map === "object" && typeof opt.mapOptions.map.defaultPlot === "object")1279)) {1280// Show all elements on the map before updating the legends1281$("[data-type='legend-elem']", self.$container).each(function (id, elem) {1282if ($(elem).attr('data-hidden') === "1") {1283$(elem).trigger("click", {hideOtherElems: false, animDuration: animDuration});1284}1285});1286
1287self.createLegends("area", self.areas, 1);1288if (self.options.map.width) {1289self.createLegends("plot", self.plots, (self.options.map.width / self.mapConf.width));1290} else {1291self.createLegends("plot", self.plots, (self.$map.width() / self.mapConf.width));1292}1293}1294
1295// Hide/Show all elements based on showlegendElems1296// Toggle (i.e. click) only if:1297// - slice legend is shown AND we want to hide1298// - slice legend is hidden AND we want to show1299if (typeof opt.setLegendElemsState === "object") {1300// setLegendElemsState is an object listing the legend we want to hide/show1301$.each(opt.setLegendElemsState, function (legendCSSClass, action) {1302// Search for the legend1303var $legend = self.$container.find("." + legendCSSClass)[0];1304if ($legend !== undefined) {1305// Select all elem inside this legend1306$("[data-type='legend-elem']", $legend).each(function (id, elem) {1307if (($(elem).attr('data-hidden') === "0" && action === "hide") ||1308($(elem).attr('data-hidden') === "1" && action === "show")) {1309// Toggle state of element by clicking1310$(elem).trigger("click", {hideOtherElems: false, animDuration: animDuration});1311}1312});1313}1314});1315} else {1316// setLegendElemsState is a string, or is undefined1317// Default : "show"1318var action = (opt.setLegendElemsState === "hide") ? "hide" : "show";1319
1320$("[data-type='legend-elem']", self.$container).each(function (id, elem) {1321if (($(elem).attr('data-hidden') === "0" && action === "hide") ||1322($(elem).attr('data-hidden') === "1" && action === "show")) {1323// Toggle state of element by clicking1324$(elem).trigger("click", {hideOtherElems: false, animDuration: animDuration});1325}1326});1327}1328
1329// Always rebind custom events on update1330self.initDelegatedCustomEvents();1331
1332if (opt.afterUpdate) opt.afterUpdate(self.$container, self.paper, self.areas, self.plots, self.options, self.links);1333},1334
1335/*1336* Set plot coordinates
1337* @param plot object plot element
1338*/
1339setPlotCoords: function(plot) {1340var self = this;1341
1342if (plot.options.x !== undefined && plot.options.y !== undefined) {1343plot.coords = {1344x: plot.options.x,1345y: plot.options.y1346};1347} else if (plot.options.plotsOn !== undefined && self.areas[plot.options.plotsOn] !== undefined) {1348var areaBBox = self.areas[plot.options.plotsOn].mapElem.getBBox();1349plot.coords = {1350x: areaBBox.cx,1351y: areaBBox.cy1352};1353} else {1354plot.coords = self.mapConf.getCoords(plot.options.latitude, plot.options.longitude);1355}1356},1357
1358/*1359* Set plot size attributes according to its type
1360* Note: for SVG, plot.mapElem needs to exists beforehand
1361* @param plot object plot element
1362*/
1363setPlotAttributes: function(plot) {1364if (plot.options.type === "square") {1365plot.options.attrs.width = plot.options.size;1366plot.options.attrs.height = plot.options.size;1367plot.options.attrs.x = plot.coords.x - (plot.options.size / 2);1368plot.options.attrs.y = plot.coords.y - (plot.options.size / 2);1369} else if (plot.options.type === "image") {1370plot.options.attrs.src = plot.options.url;1371plot.options.attrs.width = plot.options.width;1372plot.options.attrs.height = plot.options.height;1373plot.options.attrs.x = plot.coords.x - (plot.options.width / 2);1374plot.options.attrs.y = plot.coords.y - (plot.options.height / 2);1375} else if (plot.options.type === "svg") {1376plot.options.attrs.path = plot.options.path;1377
1378// Init transform string1379if (plot.options.attrs.transform === undefined) {1380plot.options.attrs.transform = "";1381}1382
1383// Retrieve original boundary box if not defined1384if (plot.mapElem.originalBBox === undefined) {1385plot.mapElem.originalBBox = plot.mapElem.getBBox();1386}1387
1388// The base transform will resize the SVG path to the one specified by width/height1389// and also move the path to the actual coordinates1390plot.mapElem.baseTransform = "m" + (plot.options.width / plot.mapElem.originalBBox.width) + ",0,0," +1391(plot.options.height / plot.mapElem.originalBBox.height) + "," +1392(plot.coords.x - plot.options.width / 2) + "," +1393(plot.coords.y - plot.options.height / 2);1394
1395plot.options.attrs.transform = plot.mapElem.baseTransform + plot.options.attrs.transform;1396
1397} else { // Default : circle1398plot.options.attrs.x = plot.coords.x;1399plot.options.attrs.y = plot.coords.y;1400plot.options.attrs.r = plot.options.size / 2;1401}1402},1403
1404/*1405* Draw all links between plots on the paper
1406*/
1407drawLinksCollection: function (linksCollection) {1408var self = this;1409var p1 = {};1410var p2 = {};1411var coordsP1 = {};1412var coordsP2 = {};1413var links = {};1414
1415$.each(linksCollection, function (id) {1416var elemOptions = self.getElemOptions(self.options.map.defaultLink, linksCollection[id], {});1417
1418if (typeof linksCollection[id].between[0] === 'string') {1419p1 = self.options.plots[linksCollection[id].between[0]];1420} else {1421p1 = linksCollection[id].between[0];1422}1423
1424if (typeof linksCollection[id].between[1] === 'string') {1425p2 = self.options.plots[linksCollection[id].between[1]];1426} else {1427p2 = linksCollection[id].between[1];1428}1429
1430if (p1.plotsOn !== undefined && self.areas[p1.plotsOn] !== undefined) {1431var p1BBox = self.areas[p1.plotsOn].mapElem.getBBox();1432coordsP1 = {1433x: p1BBox.cx,1434y: p1BBox.cy1435};1436}1437else if (p1.latitude !== undefined && p1.longitude !== undefined) {1438coordsP1 = self.mapConf.getCoords(p1.latitude, p1.longitude);1439} else {1440coordsP1.x = p1.x;1441coordsP1.y = p1.y;1442}1443
1444if (p2.plotsOn !== undefined && self.areas[p2.plotsOn] !== undefined) {1445var p2BBox = self.areas[p2.plotsOn].mapElem.getBBox();1446coordsP2 = {1447x: p2BBox.cx,1448y: p2BBox.cy1449};1450}1451else if (p2.latitude !== undefined && p2.longitude !== undefined) {1452coordsP2 = self.mapConf.getCoords(p2.latitude, p2.longitude);1453} else {1454coordsP2.x = p2.x;1455coordsP2.y = p2.y;1456}1457links[id] = self.drawLink(id, coordsP1.x, coordsP1.y, coordsP2.x, coordsP2.y, elemOptions);1458});1459return links;1460},1461
1462/*1463* Draw a curved link between two couples of coordinates a(xa,ya) and b(xb, yb) on the paper
1464*/
1465drawLink: function (id, xa, ya, xb, yb, elemOptions) {1466var self = this;1467var link = {1468options: elemOptions1469};1470// Compute the "curveto" SVG point, d(x,y)1471// c(xc, yc) is the center of (xa,ya) and (xb, yb)1472var xc = (xa + xb) / 2;1473var yc = (ya + yb) / 2;1474
1475// Equation for (cd) : y = acd * x + bcd (d is the cure point)1476var acd = -1 / ((yb - ya) / (xb - xa));1477var bcd = yc - acd * xc;1478
1479// dist(c,d) = dist(a,b) (=abDist)1480var abDist = Math.sqrt((xb - xa) * (xb - xa) + (yb - ya) * (yb - ya));1481
1482// Solution for equation dist(cd) = sqrt((xd - xc)² + (yd - yc)²)1483// dist(c,d)² = (xd - xc)² + (yd - yc)²1484// We assume that dist(c,d) = dist(a,b)1485// so : (xd - xc)² + (yd - yc)² - dist(a,b)² = 01486// With the factor : (xd - xc)² + (yd - yc)² - (factor*dist(a,b))² = 01487// (xd - xc)² + (acd*xd + bcd - yc)² - (factor*dist(a,b))² = 01488var a = 1 + acd * acd;1489var b = -2 * xc + 2 * acd * bcd - 2 * acd * yc;1490var c = xc * xc + bcd * bcd - bcd * yc - yc * bcd + yc * yc - ((elemOptions.factor * abDist) * (elemOptions.factor * abDist));1491var delta = b * b - 4 * a * c;1492var x = 0;1493var y = 0;1494
1495// There are two solutions, we choose one or the other depending on the sign of the factor1496if (elemOptions.factor > 0) {1497x = (-b + Math.sqrt(delta)) / (2 * a);1498y = acd * x + bcd;1499} else {1500x = (-b - Math.sqrt(delta)) / (2 * a);1501y = acd * x + bcd;1502}1503
1504link.mapElem = self.paper.path("m " + xa + "," + ya + " C " + x + "," + y + " " + xb + "," + yb + " " + xb + "," + yb + "");1505
1506self.initElem(id, 'link', link);1507
1508return link;1509},1510
1511/*1512* Check wether newAttrs object bring modifications to originalAttrs object
1513*/
1514isAttrsChanged: function(originalAttrs, newAttrs) {1515for (var key in newAttrs) {1516if (newAttrs.hasOwnProperty(key) && typeof originalAttrs[key] === 'undefined' || newAttrs[key] !== originalAttrs[key]) {1517return true;1518}1519}1520return false;1521},1522
1523/*1524* Update the element "elem" on the map with the new options
1525*/
1526updateElem: function (elem, animDuration) {1527var self = this;1528var mapElemBBox;1529var plotOffsetX;1530var plotOffsetY;1531
1532if (elem.options.toFront === true) {1533elem.mapElem.toFront();1534}1535
1536// Set the cursor attribute related to the HTML link1537if (elem.options.href !== undefined) {1538elem.options.attrs.cursor = "pointer";1539if (elem.options.text) elem.options.text.attrs.cursor = "pointer";1540} else {1541// No HTML links, check if a cursor was defined to pointer1542if (elem.mapElem.attrs.cursor === 'pointer') {1543elem.options.attrs.cursor = "auto";1544if (elem.options.text) elem.options.text.attrs.cursor = "auto";1545}1546}1547
1548// Update the label1549if (elem.textElem) {1550// Update text attr1551elem.options.text.attrs.text = elem.options.text.content;1552
1553// Get mapElem size, and apply an offset to handle future width/height change1554mapElemBBox = elem.mapElem.getBBox();1555if (elem.options.size || (elem.options.width && elem.options.height)) {1556if (elem.options.type === "image" || elem.options.type === "svg") {1557plotOffsetX = (elem.options.width - mapElemBBox.width) / 2;1558plotOffsetY = (elem.options.height - mapElemBBox.height) / 2;1559} else {1560plotOffsetX = (elem.options.size - mapElemBBox.width) / 2;1561plotOffsetY = (elem.options.size - mapElemBBox.height) / 2;1562}1563mapElemBBox.x -= plotOffsetX;1564mapElemBBox.x2 += plotOffsetX;1565mapElemBBox.y -= plotOffsetY;1566mapElemBBox.y2 += plotOffsetY;1567}1568
1569// Update position attr1570var textPosition = self.getTextPosition(mapElemBBox, elem.options.text.position, elem.options.text.margin);1571elem.options.text.attrs.x = textPosition.x;1572elem.options.text.attrs.y = textPosition.y;1573elem.options.text.attrs['text-anchor'] = textPosition.textAnchor;1574
1575// Update text element attrs and attrsHover1576self.setHoverOptions(elem.textElem, elem.options.text.attrs, elem.options.text.attrsHover);1577
1578if (self.isAttrsChanged(elem.textElem.attrs, elem.options.text.attrs)) {1579self.animate(elem.textElem, elem.options.text.attrs, animDuration);1580}1581}1582
1583// Update elements attrs and attrsHover1584self.setHoverOptions(elem.mapElem, elem.options.attrs, elem.options.attrsHover);1585
1586if (self.isAttrsChanged(elem.mapElem.attrs, elem.options.attrs)) {1587self.animate(elem.mapElem, elem.options.attrs, animDuration);1588}1589
1590// Update the cssClass1591if (elem.options.cssClass !== undefined) {1592$(elem.mapElem.node).removeClass().addClass(elem.options.cssClass);1593}1594},1595
1596/*1597* Draw the plot
1598*/
1599drawPlot: function (id) {1600var self = this;1601var plot = {};1602
1603// Get plot options and store it1604plot.options = self.getElemOptions(1605self.options.map.defaultPlot,1606(self.options.plots[id] ? self.options.plots[id] : {}),1607self.options.legend.plot1608);1609
1610// Set plot coords1611self.setPlotCoords(plot);1612
1613// Draw SVG before setPlotAttributes()1614if (plot.options.type === "svg") {1615plot.mapElem = self.paper.path(plot.options.path);1616}1617
1618// Set plot size attrs1619self.setPlotAttributes(plot);1620
1621// Draw other types of plots1622if (plot.options.type === "square") {1623plot.mapElem = self.paper.rect(1624plot.options.attrs.x,1625plot.options.attrs.y,1626plot.options.attrs.width,1627plot.options.attrs.height1628);1629} else if (plot.options.type === "image") {1630plot.mapElem = self.paper.image(1631plot.options.attrs.src,1632plot.options.attrs.x,1633plot.options.attrs.y,1634plot.options.attrs.width,1635plot.options.attrs.height1636);1637} else if (plot.options.type === "svg") {1638// Nothing to do1639} else {1640// Default = circle1641plot.mapElem = self.paper.circle(1642plot.options.attrs.x,1643plot.options.attrs.y,1644plot.options.attrs.r1645);1646}1647
1648self.initElem(id, 'plot', plot);1649
1650return plot;1651},1652
1653/*1654* Set user defined handlers for events on areas and plots
1655* @param id the id of the element
1656* @param type the type of the element (area, plot, link)
1657* @param elem the element object {mapElem, textElem, options, ...}
1658*/
1659setEventHandlers: function (id, type, elem) {1660var self = this;1661$.each(elem.options.eventHandlers, function (event) {1662if (self.customEventHandlers[event] === undefined) self.customEventHandlers[event] = {};1663if (self.customEventHandlers[event][type] === undefined) self.customEventHandlers[event][type] = {};1664self.customEventHandlers[event][type][id] = elem;1665});1666},1667
1668/*1669* Draw a legend for areas and / or plots
1670* @param legendOptions options for the legend to draw
1671* @param legendType the type of the legend : "area" or "plot"
1672* @param elems collection of plots or areas on the maps
1673* @param legendIndex index of the legend in the conf array
1674*/
1675drawLegend: function (legendOptions, legendType, elems, scale, legendIndex) {1676var self = this;1677var $legend = {};1678var legendPaper = {};1679var width = 0;1680var height = 0;1681var title = null;1682var titleBBox = null;1683var legendElems = {};1684var i = 0;1685var x = 0;1686var y = 0;1687var yCenter = 0;1688var sliceOptions = [];1689
1690$legend = $("." + legendOptions.cssClass, self.$container);1691
1692// Save content for later1693var initialHTMLContent = $legend.html();1694$legend.empty();1695
1696legendPaper = new Raphael($legend.get(0));1697// Set some data to object1698$(legendPaper.canvas).attr({"data-legend-type": legendType, "data-legend-id": legendIndex});1699
1700height = width = 0;1701
1702// Set the title of the legend1703if (legendOptions.title && legendOptions.title !== "") {1704title = legendPaper.text(legendOptions.marginLeftTitle, 0, legendOptions.title).attr(legendOptions.titleAttrs);1705titleBBox = title.getBBox();1706title.attr({y: 0.5 * titleBBox.height});1707
1708width = legendOptions.marginLeftTitle + titleBBox.width;1709height += legendOptions.marginBottomTitle + titleBBox.height;1710}1711
1712// Calculate attrs (and width, height and r (radius)) for legend elements, and yCenter for horizontal legends1713
1714for (i = 0; i < legendOptions.slices.length; ++i) {1715var yCenterCurrent = 0;1716
1717sliceOptions[i] = $.extend(true, {}, (legendType === "plot") ? self.options.map.defaultPlot : self.options.map.defaultArea, legendOptions.slices[i]);1718
1719if (legendOptions.slices[i].legendSpecificAttrs === undefined) {1720legendOptions.slices[i].legendSpecificAttrs = {};1721}1722
1723$.extend(true, sliceOptions[i].attrs, legendOptions.slices[i].legendSpecificAttrs);1724
1725if (legendType === "area") {1726if (sliceOptions[i].attrs.width === undefined)1727sliceOptions[i].attrs.width = 30;1728if (sliceOptions[i].attrs.height === undefined)1729sliceOptions[i].attrs.height = 20;1730} else if (sliceOptions[i].type === "square") {1731if (sliceOptions[i].attrs.width === undefined)1732sliceOptions[i].attrs.width = sliceOptions[i].size;1733if (sliceOptions[i].attrs.height === undefined)1734sliceOptions[i].attrs.height = sliceOptions[i].size;1735} else if (sliceOptions[i].type === "image" || sliceOptions[i].type === "svg") {1736if (sliceOptions[i].attrs.width === undefined)1737sliceOptions[i].attrs.width = sliceOptions[i].width;1738if (sliceOptions[i].attrs.height === undefined)1739sliceOptions[i].attrs.height = sliceOptions[i].height;1740} else {1741if (sliceOptions[i].attrs.r === undefined)1742sliceOptions[i].attrs.r = sliceOptions[i].size / 2;1743}1744
1745// Compute yCenter for this legend slice1746yCenterCurrent = legendOptions.marginBottomTitle;1747// Add title height if it exists1748if (title) {1749yCenterCurrent += titleBBox.height;1750}1751if (legendType === "plot" && (sliceOptions[i].type === undefined || sliceOptions[i].type === "circle")) {1752yCenterCurrent += scale * sliceOptions[i].attrs.r;1753} else {1754yCenterCurrent += scale * sliceOptions[i].attrs.height / 2;1755}1756// Update yCenter if current larger1757yCenter = Math.max(yCenter, yCenterCurrent);1758}1759
1760if (legendOptions.mode === "horizontal") {1761width = legendOptions.marginLeft;1762}1763
1764// Draw legend elements (circle, square or image in vertical or horizontal mode)1765for (i = 0; i < sliceOptions.length; ++i) {1766var legendElem = {};1767var legendElemBBox = {};1768var legendLabel = {};1769
1770if (sliceOptions[i].display === undefined || sliceOptions[i].display === true) {1771if (legendType === "area") {1772if (legendOptions.mode === "horizontal") {1773x = width + legendOptions.marginLeft;1774y = yCenter - (0.5 * scale * sliceOptions[i].attrs.height);1775} else {1776x = legendOptions.marginLeft;1777y = height;1778}1779
1780legendElem = legendPaper.rect(x, y, scale * (sliceOptions[i].attrs.width), scale * (sliceOptions[i].attrs.height));1781} else if (sliceOptions[i].type === "square") {1782if (legendOptions.mode === "horizontal") {1783x = width + legendOptions.marginLeft;1784y = yCenter - (0.5 * scale * sliceOptions[i].attrs.height);1785} else {1786x = legendOptions.marginLeft;1787y = height;1788}1789
1790legendElem = legendPaper.rect(x, y, scale * (sliceOptions[i].attrs.width), scale * (sliceOptions[i].attrs.height));1791
1792} else if (sliceOptions[i].type === "image" || sliceOptions[i].type === "svg") {1793if (legendOptions.mode === "horizontal") {1794x = width + legendOptions.marginLeft;1795y = yCenter - (0.5 * scale * sliceOptions[i].attrs.height);1796} else {1797x = legendOptions.marginLeft;1798y = height;1799}1800
1801if (sliceOptions[i].type === "image") {1802legendElem = legendPaper.image(1803sliceOptions[i].url, x, y, scale * sliceOptions[i].attrs.width, scale * sliceOptions[i].attrs.height);1804} else {1805legendElem = legendPaper.path(sliceOptions[i].path);1806
1807if (sliceOptions[i].attrs.transform === undefined) {1808sliceOptions[i].attrs.transform = "";1809}1810legendElemBBox = legendElem.getBBox();1811sliceOptions[i].attrs.transform = "m" + ((scale * sliceOptions[i].width) / legendElemBBox.width) + ",0,0," + ((scale * sliceOptions[i].height) / legendElemBBox.height) + "," + x + "," + y + sliceOptions[i].attrs.transform;1812}1813} else {1814if (legendOptions.mode === "horizontal") {1815x = width + legendOptions.marginLeft + scale * (sliceOptions[i].attrs.r);1816y = yCenter;1817} else {1818x = legendOptions.marginLeft + scale * (sliceOptions[i].attrs.r);1819y = height + scale * (sliceOptions[i].attrs.r);1820}1821legendElem = legendPaper.circle(x, y, scale * (sliceOptions[i].attrs.r));1822}1823
1824// Set attrs to the element drawn above1825delete sliceOptions[i].attrs.width;1826delete sliceOptions[i].attrs.height;1827delete sliceOptions[i].attrs.r;1828legendElem.attr(sliceOptions[i].attrs);1829legendElemBBox = legendElem.getBBox();1830
1831// Draw the label associated with the element1832if (legendOptions.mode === "horizontal") {1833x = width + legendOptions.marginLeft + legendElemBBox.width + legendOptions.marginLeftLabel;1834y = yCenter;1835} else {1836x = legendOptions.marginLeft + legendElemBBox.width + legendOptions.marginLeftLabel;1837y = height + (legendElemBBox.height / 2);1838}1839
1840legendLabel = legendPaper.text(x, y, sliceOptions[i].label).attr(legendOptions.labelAttrs);1841
1842// Update the width and height for the paper1843if (legendOptions.mode === "horizontal") {1844var currentHeight = legendOptions.marginBottom + legendElemBBox.height;1845width += legendOptions.marginLeft + legendElemBBox.width + legendOptions.marginLeftLabel + legendLabel.getBBox().width;1846if (sliceOptions[i].type !== "image" && legendType !== "area") {1847currentHeight += legendOptions.marginBottomTitle;1848}1849// Add title height if it exists1850if (title) {1851currentHeight += titleBBox.height;1852}1853height = Math.max(height, currentHeight);1854} else {1855width = Math.max(width, legendOptions.marginLeft + legendElemBBox.width + legendOptions.marginLeftLabel + legendLabel.getBBox().width);1856height += legendOptions.marginBottom + legendElemBBox.height;1857}1858
1859// Set some data to elements1860$(legendElem.node).attr({1861"data-legend-id": legendIndex,1862"data-legend-type": legendType,1863"data-type": "legend-elem",1864"data-id": i,1865"data-hidden": 01866});1867$(legendLabel.node).attr({1868"data-legend-id": legendIndex,1869"data-legend-type": legendType,1870"data-type": "legend-label",1871"data-id": i,1872"data-hidden": 01873});1874
1875// Set array content1876// We use similar names like map/plots/links1877legendElems[i] = {1878mapElem: legendElem,1879textElem: legendLabel1880};1881
1882// Hide map elements when the user clicks on a legend item1883if (legendOptions.hideElemsOnClick.enabled) {1884// Hide/show elements when user clicks on a legend element1885legendLabel.attr({cursor: "pointer"});1886legendElem.attr({cursor: "pointer"});1887
1888self.setHoverOptions(legendElem, sliceOptions[i].attrs, sliceOptions[i].attrs);1889self.setHoverOptions(legendLabel, legendOptions.labelAttrs, legendOptions.labelAttrsHover);1890
1891if (sliceOptions[i].clicked !== undefined && sliceOptions[i].clicked === true) {1892self.handleClickOnLegendElem(legendElems[i], i, legendIndex, legendType, {hideOtherElems: false});1893}1894}1895}1896}1897
1898// VMLWidth option allows you to set static width for the legend1899// only for VML render because text.getBBox() returns wrong values on IE6/71900if (Raphael.type !== "SVG" && legendOptions.VMLWidth)1901width = legendOptions.VMLWidth;1902
1903legendPaper.setSize(width, height);1904
1905return {1906container: $legend,1907initialHTMLContent: initialHTMLContent,1908elems: legendElems1909};1910},1911
1912/*1913* Allow to hide elements of the map when the user clicks on a related legend item
1914* @param elem legend element
1915* @param id legend element ID
1916* @param legendIndex corresponding legend index
1917* @param legendType corresponding legend type (area or plot)
1918* @param opts object additionnal options
1919* hideOtherElems boolean, if other elems shall be hidden
1920* animDuration duration of animation
1921*/
1922handleClickOnLegendElem: function(elem, id, legendIndex, legendType, opts) {1923var self = this;1924var legendOptions;1925opts = opts || {};1926
1927if (!$.isArray(self.options.legend[legendType])) {1928legendOptions = self.options.legend[legendType];1929} else {1930legendOptions = self.options.legend[legendType][legendIndex];1931}1932
1933var legendElem = elem.mapElem;1934var legendLabel = elem.textElem;1935var $legendElem = $(legendElem.node);1936var $legendLabel = $(legendLabel.node);1937var sliceOptions = legendOptions.slices[id];1938var mapElems = legendType === 'area' ? self.areas : self.plots;1939// Check animDuration: if not set, this is a regular click, use the value specified in options1940var animDuration = opts.animDuration !== undefined ? opts.animDuration : legendOptions.hideElemsOnClick.animDuration ;1941
1942var hidden = $legendElem.attr('data-hidden');1943var hiddenNewAttr = (hidden === '0') ? {"data-hidden": '1'} : {"data-hidden": '0'};1944
1945if (hidden === '0') {1946self.animate(legendLabel, {"opacity": 0.5}, animDuration);1947} else {1948self.animate(legendLabel, {"opacity": 1}, animDuration);1949}1950
1951$.each(mapElems, function (y) {1952var elemValue;1953
1954// Retreive stored data of element1955// 'hidden-by' contains the list of legendIndex that is hiding this element1956var hiddenBy = mapElems[y].mapElem.data('hidden-by');1957// Set to empty object if undefined1958if (hiddenBy === undefined) hiddenBy = {};1959
1960if ($.isArray(mapElems[y].options.value)) {1961elemValue = mapElems[y].options.value[legendIndex];1962} else {1963elemValue = mapElems[y].options.value;1964}1965
1966// Hide elements whose value matches with the slice of the clicked legend item1967if (self.getLegendSlice(elemValue, legendOptions) === sliceOptions) {1968if (hidden === '0') { // we want to hide this element1969hiddenBy[legendIndex] = true; // add legendIndex to the data object for later use1970self.setElementOpacity(mapElems[y], legendOptions.hideElemsOnClick.opacity, animDuration);1971} else { // We want to show this element1972delete hiddenBy[legendIndex]; // Remove this legendIndex from object1973// Check if another legendIndex is defined1974// We will show this element only if no legend is no longer hiding it1975if ($.isEmptyObject(hiddenBy)) {1976self.setElementOpacity(1977mapElems[y],1978mapElems[y].mapElem.originalAttrs.opacity !== undefined ? mapElems[y].mapElem.originalAttrs.opacity : 1,1979animDuration
1980);1981}1982}1983// Update elem data with new values1984mapElems[y].mapElem.data('hidden-by', hiddenBy);1985}1986});1987
1988$legendElem.attr(hiddenNewAttr);1989$legendLabel.attr(hiddenNewAttr);1990
1991if ((opts.hideOtherElems === undefined || opts.hideOtherElems === true) && legendOptions.exclusive === true ) {1992$("[data-type='legend-elem'][data-hidden=0]", self.$container).each(function () {1993var $elem = $(this);1994if ($elem.attr('data-id') !== id) {1995$elem.trigger("click", {hideOtherElems: false});1996}1997});1998}1999
2000},2001
2002/*2003* Create all legends for a specified type (area or plot)
2004* @param legendType the type of the legend : "area" or "plot"
2005* @param elems collection of plots or areas displayed on the map
2006* @param scale scale ratio of the map
2007*/
2008createLegends: function (legendType, elems, scale) {2009var self = this;2010var legendsOptions = self.options.legend[legendType];2011
2012if (!$.isArray(self.options.legend[legendType])) {2013legendsOptions = [self.options.legend[legendType]];2014}2015
2016self.legends[legendType] = {};2017for (var j = 0; j < legendsOptions.length; ++j) {2018if (legendsOptions[j].display === true && $.isArray(legendsOptions[j].slices) && legendsOptions[j].slices.length > 0 &&2019legendsOptions[j].cssClass !== "" && $("." + legendsOptions[j].cssClass, self.$container).length !== 02020) {2021self.legends[legendType][j] = self.drawLegend(legendsOptions[j], legendType, elems, scale, j);2022}2023}2024},2025
2026/*2027* Set the attributes on hover and the attributes to restore for a map element
2028* @param elem the map element
2029* @param originalAttrs the original attributes to restore on mouseout event
2030* @param attrsHover the attributes to set on mouseover event
2031*/
2032setHoverOptions: function (elem, originalAttrs, attrsHover) {2033// Disable transform option on hover for VML (IE<9) because of several bugs2034if (Raphael.type !== "SVG") delete attrsHover.transform;2035elem.attrsHover = attrsHover;2036
2037if (elem.attrsHover.transform) elem.originalAttrs = $.extend({transform: "s1"}, originalAttrs);2038else elem.originalAttrs = originalAttrs;2039},2040
2041/*2042* Set the behaviour when mouse enters element ("mouseover" event)
2043* It may be an area, a plot, a link or a legend element
2044* @param elem the map element
2045*/
2046elemEnter: function (elem) {2047var self = this;2048if (elem === undefined) return;2049
2050/* Handle mapElem Hover attributes */2051if (elem.mapElem !== undefined) {2052self.animate(elem.mapElem, elem.mapElem.attrsHover, elem.mapElem.attrsHover.animDuration);2053}2054
2055/* Handle textElem Hover attributes */2056if (elem.textElem !== undefined) {2057self.animate(elem.textElem, elem.textElem.attrsHover, elem.textElem.attrsHover.animDuration);2058}2059
2060/* Handle tooltip init */2061if (elem.options && elem.options.tooltip !== undefined) {2062var content = '';2063// Reset classes2064self.$tooltip.removeClass().addClass(self.options.map.tooltip.cssClass);2065// Get content2066if (elem.options.tooltip.content !== undefined) {2067// if tooltip.content is function, call it. Otherwise, assign it directly.2068if (typeof elem.options.tooltip.content === "function") content = elem.options.tooltip.content(elem.mapElem);2069else content = elem.options.tooltip.content;2070}2071if (elem.options.tooltip.cssClass !== undefined) {2072self.$tooltip.addClass(elem.options.tooltip.cssClass);2073}2074self.$tooltip.html(content).css("display", "block");2075}2076
2077// workaround for older version of Raphael2078if (elem.mapElem !== undefined || elem.textElem !== undefined) {2079if (self.paper.safari) self.paper.safari();2080}2081},2082
2083/*2084* Set the behaviour when mouse moves in element ("mousemove" event)
2085* @param elem the map element
2086*/
2087elemHover: function (elem, event) {2088var self = this;2089if (elem === undefined) return;2090
2091/* Handle tooltip position update */2092if (elem.options.tooltip !== undefined) {2093var mouseX = event.pageX;2094var mouseY = event.pageY;2095
2096var offsetLeft = 10;2097var offsetTop = 20;2098if (typeof elem.options.tooltip.offset === "object") {2099if (typeof elem.options.tooltip.offset.left !== "undefined") {2100offsetLeft = elem.options.tooltip.offset.left;2101}2102if (typeof elem.options.tooltip.offset.top !== "undefined") {2103offsetTop = elem.options.tooltip.offset.top;2104}2105}2106
2107var tooltipPosition = {2108"left": Math.min(self.$map.width() - self.$tooltip.outerWidth() - 5,2109mouseX - self.$map.offset().left + offsetLeft),2110"top": Math.min(self.$map.height() - self.$tooltip.outerHeight() - 5,2111mouseY - self.$map.offset().top + offsetTop)2112};2113
2114if (typeof elem.options.tooltip.overflow === "object") {2115if (elem.options.tooltip.overflow.right === true) {2116tooltipPosition.left = mouseX - self.$map.offset().left + 10;2117}2118if (elem.options.tooltip.overflow.bottom === true) {2119tooltipPosition.top = mouseY - self.$map.offset().top + 20;2120}2121}2122
2123self.$tooltip.css(tooltipPosition);2124}2125},2126
2127/*2128* Set the behaviour when mouse leaves element ("mouseout" event)
2129* It may be an area, a plot, a link or a legend element
2130* @param elem the map element
2131*/
2132elemOut: function (elem) {2133var self = this;2134if (elem === undefined) return;2135
2136/* reset mapElem attributes */2137if (elem.mapElem !== undefined) {2138self.animate(elem.mapElem, elem.mapElem.originalAttrs, elem.mapElem.attrsHover.animDuration);2139}2140
2141/* reset textElem attributes */2142if (elem.textElem !== undefined) {2143self.animate(elem.textElem, elem.textElem.originalAttrs, elem.textElem.attrsHover.animDuration);2144}2145
2146/* reset tooltip */2147if (elem.options && elem.options.tooltip !== undefined) {2148self.$tooltip.css({2149'display': 'none',2150'top': -1000,2151'left': -10002152});2153}2154
2155// workaround for older version of Raphael2156if (elem.mapElem !== undefined || elem.textElem !== undefined) {2157if (self.paper.safari) self.paper.safari();2158}2159},2160
2161/*2162* Set the behaviour when mouse clicks element ("click" event)
2163* It may be an area, a plot or a link (but not a legend element which has its own function)
2164* @param elem the map element
2165*/
2166elemClick: function (elem) {2167var self = this;2168if (elem === undefined) return;2169
2170/* Handle click when href defined */2171if (!self.panning && elem.options.href !== undefined) {2172window.open(elem.options.href, elem.options.target);2173}2174},2175
2176/*2177* Get element options by merging default options, element options and legend options
2178* @param defaultOptions
2179* @param elemOptions
2180* @param legendOptions
2181*/
2182getElemOptions: function (defaultOptions, elemOptions, legendOptions) {2183var self = this;2184var options = $.extend(true, {}, defaultOptions, elemOptions);2185if (options.value !== undefined) {2186if ($.isArray(legendOptions)) {2187for (var i = 0; i < legendOptions.length; ++i) {2188options = $.extend(true, {}, options, self.getLegendSlice(options.value[i], legendOptions[i]));2189}2190} else {2191options = $.extend(true, {}, options, self.getLegendSlice(options.value, legendOptions));2192}2193}2194return options;2195},2196
2197/*2198* Get the coordinates of the text relative to a bbox and a position
2199* @param bbox the boundary box of the element
2200* @param textPosition the wanted text position (inner, right, left, top or bottom)
2201* @param margin number or object {x: val, y:val} margin between the bbox and the text
2202*/
2203getTextPosition: function (bbox, textPosition, margin) {2204var textX = 0;2205var textY = 0;2206var textAnchor = "";2207
2208if (typeof margin === "number") {2209if (textPosition === "bottom" || textPosition === "top") {2210margin = {x: 0, y: margin};2211} else if (textPosition === "right" || textPosition === "left") {2212margin = {x: margin, y: 0};2213} else {2214margin = {x: 0, y: 0};2215}2216}2217
2218switch (textPosition) {2219case "bottom" :2220textX = ((bbox.x + bbox.x2) / 2) + margin.x;2221textY = bbox.y2 + margin.y;2222textAnchor = "middle";2223break;2224case "top" :2225textX = ((bbox.x + bbox.x2) / 2) + margin.x;2226textY = bbox.y - margin.y;2227textAnchor = "middle";2228break;2229case "left" :2230textX = bbox.x - margin.x;2231textY = ((bbox.y + bbox.y2) / 2) + margin.y;2232textAnchor = "end";2233break;2234case "right" :2235textX = bbox.x2 + margin.x;2236textY = ((bbox.y + bbox.y2) / 2) + margin.y;2237textAnchor = "start";2238break;2239default : // "inner" position2240textX = ((bbox.x + bbox.x2) / 2) + margin.x;2241textY = ((bbox.y + bbox.y2) / 2) + margin.y;2242textAnchor = "middle";2243}2244return {"x": textX, "y": textY, "textAnchor": textAnchor};2245},2246
2247/*2248* Get the legend conf matching with the value
2249* @param value the value to match with a slice in the legend
2250* @param legend the legend params object
2251* @return the legend slice matching with the value
2252*/
2253getLegendSlice: function (value, legend) {2254for (var i = 0; i < legend.slices.length; ++i) {2255if ((legend.slices[i].sliceValue !== undefined && value === legend.slices[i].sliceValue) ||2256((legend.slices[i].sliceValue === undefined) &&2257(legend.slices[i].min === undefined || value >= legend.slices[i].min) &&2258(legend.slices[i].max === undefined || value <= legend.slices[i].max))2259) {2260return legend.slices[i];2261}2262}2263return {};2264},2265
2266/*2267* Animated view box changes
2268* As from http://code.voidblossom.com/animating-viewbox-easing-formulas/,
2269* (from https://github.com/theshaun works on mapael)
2270* @param x coordinate of the point to focus on
2271* @param y coordinate of the point to focus on
2272* @param w map defined width
2273* @param h map defined height
2274* @param duration defined length of time for animation
2275* @param easingFunction defined Raphael supported easing_formula to use
2276*/
2277animateViewBox: function (targetX, targetY, targetW, targetH, duration, easingFunction) {2278var self = this;2279
2280var cx = self.currentViewBox.x;2281var dx = targetX - cx;2282var cy = self.currentViewBox.y;2283var dy = targetY - cy;2284var cw = self.currentViewBox.w;2285var dw = targetW - cw;2286var ch = self.currentViewBox.h;2287var dh = targetH - ch;2288
2289// Init current ViewBox target if undefined2290if (!self.zoomAnimCVBTarget) {2291self.zoomAnimCVBTarget = {2292x: targetX, y: targetY, w: targetW, h: targetH2293};2294}2295
2296// Determine zoom direction by comparig current vs. target width2297var zoomDir = (cw > targetW) ? 'in' : 'out';2298
2299var easingFormula = Raphael.easing_formulas[easingFunction || "linear"];2300
2301// To avoid another frame when elapsed time approach end (2%)2302var durationWithMargin = duration - (duration * 2 / 100);2303
2304// Save current zoomAnimStartTime before assigning a new one2305var oldZoomAnimStartTime = self.zoomAnimStartTime;2306self.zoomAnimStartTime = (new Date()).getTime();2307
2308/* Actual function to animate the ViewBox2309* Uses requestAnimationFrame to schedule itself again until animation is over
2310*/
2311var computeNextStep = function () {2312// Cancel any remaining animationFrame2313// It means this new step will take precedence over the old one scheduled2314// This is the case when the user is triggering the zoom fast (e.g. with a big mousewheel run)2315// This actually does nothing when performing a single zoom action2316self.cancelAnimationFrame(self.zoomAnimID);2317// Compute elapsed time2318var elapsed = (new Date()).getTime() - self.zoomAnimStartTime;2319// Check if animation should finish2320if (elapsed < durationWithMargin) {2321// Hold the future ViewBox values2322var x, y, w, h;2323
2324// There are two ways to compute the next ViewBox size2325// 1. If the target ViewBox has changed between steps (=> ADAPTATION step)2326// 2. Or if the target ViewBox is the same (=> NORMAL step)2327//2328// A change of ViewBox target between steps means the user is triggering2329// the zoom fast (like a big scroll with its mousewheel)2330//2331// The new animation step with the new target will always take precedence over the2332// last one and start from 0 (we overwrite zoomAnimStartTime and cancel the scheduled frame)2333//2334// So if we don't detect the change of target and adapt our computation,2335// the user will see a delay at beginning the ratio will stays at 0 for some frames2336//2337// Hence when detecting the change of target, we animate from the previous target.2338//2339// The next step will then take the lead and continue from there, achieving a nicer2340// experience for user.2341
2342// Change of target IF: an old animation start value exists AND the target has actually changed2343if (oldZoomAnimStartTime && self.zoomAnimCVBTarget && self.zoomAnimCVBTarget.w !== targetW) {2344// Compute the real time elapsed with the last step2345var realElapsed = (new Date()).getTime() - oldZoomAnimStartTime;2346// Compute then the actual ratio we're at2347var realRatio = easingFormula(realElapsed / duration);2348// Compute new ViewBox values2349// The difference with the normal function is regarding the delta value used2350// We don't take the current (dx, dy, dw, dh) values yet because they are related to the new target2351// But we take the old target2352x = cx + (self.zoomAnimCVBTarget.x - cx) * realRatio;2353y = cy + (self.zoomAnimCVBTarget.y - cy) * realRatio;2354w = cw + (self.zoomAnimCVBTarget.w - cw) * realRatio;2355h = ch + (self.zoomAnimCVBTarget.h - ch) * realRatio;2356// Update cw, cy, cw and ch so the next step take animation from here2357cx = x;2358dx = targetX - cx;2359cy = y;2360dy = targetY - cy;2361cw = w;2362dw = targetW - cw;2363ch = h;2364dh = targetH - ch;2365// Update the current ViewBox target2366self.zoomAnimCVBTarget = {2367x: targetX, y: targetY, w: targetW, h: targetH2368};2369} else {2370// This is the classical approach when nothing come interrupting the zoom2371// Compute ratio according to elasped time and easing formula2372var ratio = easingFormula(elapsed / duration);2373// From the current value, we add a delta with a ratio that will leads us to the target2374x = cx + dx * ratio;2375y = cy + dy * ratio;2376w = cw + dw * ratio;2377h = ch + dh * ratio;2378}2379
2380// Some checks before applying the new viewBox2381if (zoomDir === 'in' && (w > self.currentViewBox.w || w < targetW)) {2382// Zooming IN and the new ViewBox seems larger than the current value, or smaller than target value2383// We do NOT set the ViewBox with this value2384// Otherwise, the user would see the camera going back and forth2385} else if (zoomDir === 'out' && (w < self.currentViewBox.w || w > targetW)) {2386// Zooming OUT and the new ViewBox seems smaller than the current value, or larger than target value2387// We do NOT set the ViewBox with this value2388// Otherwise, the user would see the camera going back and forth2389} else {2390// New values look good, applying2391self.setViewBox(x, y, w, h);2392}2393
2394// Schedule the next step2395self.zoomAnimID = self.requestAnimationFrame(computeNextStep);2396} else {2397/* Zoom animation done ! */2398// Perform some cleaning2399self.zoomAnimStartTime = null;2400self.zoomAnimCVBTarget = null;2401// Make sure the ViewBox hits the target!2402if (self.currentViewBox.w !== targetW) {2403self.setViewBox(targetX, targetY, targetW, targetH);2404}2405// Finally trigger afterZoom event2406self.$map.trigger("afterZoom", {2407x1: targetX, y1: targetY,2408x2: (targetX + targetW), y2: (targetY + targetH)2409});2410}2411};2412
2413// Invoke the first step directly2414computeNextStep();2415},2416
2417/*2418* requestAnimationFrame/cancelAnimationFrame polyfill
2419* Based on https://gist.github.com/jlmakes/47eba84c54bc306186ac1ab2ffd336d4
2420* and also https://gist.github.com/paulirish/1579671
2421*
2422* _requestAnimationFrameFn and _cancelAnimationFrameFn hold the current functions
2423* But requestAnimationFrame and cancelAnimationFrame shall be called since
2424* in order to be in window context
2425*/
2426// The function to use for requestAnimationFrame2427requestAnimationFrame: function(callback) {2428return this._requestAnimationFrameFn.call(window, callback);2429},2430// The function to use for cancelAnimationFrame2431cancelAnimationFrame: function(id) {2432this._cancelAnimationFrameFn.call(window, id);2433},2434// The requestAnimationFrame polyfill'd function2435// Value set by self-invoking function, will be run only once2436_requestAnimationFrameFn: (function () {2437var polyfill = (function () {2438var clock = (new Date()).getTime();2439
2440return function (callback) {2441var currentTime = (new Date()).getTime();2442
2443// requestAnimationFrame strive to run @60FPS2444// (e.g. every 16 ms)2445if (currentTime - clock > 16) {2446clock = currentTime;2447callback(currentTime);2448} else {2449// Ask browser to schedule next callback when possible2450return setTimeout(function () {2451polyfill(callback);2452}, 0);2453}2454};2455})();2456
2457return window.requestAnimationFrame ||2458window.webkitRequestAnimationFrame ||2459window.mozRequestAnimationFrame ||2460window.msRequestAnimationFrame ||2461window.oRequestAnimationFrame ||2462polyfill;2463})(),2464// The CancelAnimationFrame polyfill'd function2465// Value set by self-invoking function, will be run only once2466_cancelAnimationFrameFn: (function () {2467return window.cancelAnimationFrame ||2468window.webkitCancelAnimationFrame ||2469window.webkitCancelRequestAnimationFrame ||2470window.mozCancelAnimationFrame ||2471window.mozCancelRequestAnimationFrame ||2472window.msCancelAnimationFrame ||2473window.msCancelRequestAnimationFrame ||2474window.oCancelAnimationFrame ||2475window.oCancelRequestAnimationFrame ||2476clearTimeout;2477})(),2478
2479/*2480* SetViewBox wrapper
2481* Apply new viewbox values and keep track of them
2482*
2483* This avoid using the internal variable paper._viewBox which
2484* may not be present in future version of Raphael
2485*/
2486setViewBox: function(x, y, w, h) {2487var self = this;2488// Update current value2489self.currentViewBox.x = x;2490self.currentViewBox.y = y;2491self.currentViewBox.w = w;2492self.currentViewBox.h = h;2493// Perform set view box2494self.paper.setViewBox(x, y, w, h, false);2495},2496
2497/*2498* Animate wrapper for Raphael element
2499*
2500* Perform an animation and ensure the non-animated attr are set.
2501* This is needed for specific attributes like cursor who will not
2502* be animated, and thus not set.
2503*
2504* If duration is set to 0 (or not set), no animation are performed
2505* and attributes are directly set (and the callback directly called)
2506*/
2507// List extracted from Raphael internal vars2508// Diff between Raphael.availableAttrs and Raphael._availableAnimAttrs2509_nonAnimatedAttrs: [2510"arrow-end", "arrow-start", "gradient",2511"class", "cursor", "text-anchor",2512"font", "font-family", "font-style", "font-weight", "letter-spacing",2513"src", "href", "target", "title",2514"stroke-dasharray", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit"2515],2516/*2517* @param element Raphael element
2518* @param attrs Attributes object to animate
2519* @param duration Animation duration in ms
2520* @param callback Callback to eventually call after animation is done
2521*/
2522animate: function(element, attrs, duration, callback) {2523var self = this;2524// Check element2525if (!element) return;2526if (duration > 0) {2527// Filter out non-animated attributes2528// Note: we don't need to delete from original attribute (they won't be set anyway)2529var attrsNonAnimated = {};2530for (var i=0 ; i < self._nonAnimatedAttrs.length ; i++) {2531var attrName = self._nonAnimatedAttrs[i];2532if (attrs[attrName] !== undefined) {2533attrsNonAnimated[attrName] = attrs[attrName];2534}2535}2536// Set non-animated attributes2537element.attr(attrsNonAnimated);2538// Start animation for all attributes2539element.animate(attrs, duration, 'linear', function() {2540if (callback) callback();2541});2542} else {2543// No animation: simply set all attributes...2544element.attr(attrs);2545// ... and call the callback if needed2546if (callback) callback();2547}2548},2549
2550/*2551* Check for Raphael bug regarding drawing while beeing hidden (under display:none)
2552* See https://github.com/neveldo/jQuery-Mapael/issues/135
2553* @return true/false
2554*
2555* Wants to override this behavior? Use prototype overriding:
2556* $.mapael.prototype.isRaphaelBBoxBugPresent = function() {return false;};
2557*/
2558isRaphaelBBoxBugPresent: function() {2559var self = this;2560// Draw text, then get its boundaries2561var textElem = self.paper.text(-50, -50, "TEST");2562var textElemBBox = textElem.getBBox();2563// remove element2564textElem.remove();2565// If it has no height and width, then the paper is hidden2566return (textElemBBox.width === 0 && textElemBBox.height === 0);2567},2568
2569// Default map options2570defaultOptions: {2571map: {2572cssClass: "map",2573tooltip: {2574cssClass: "mapTooltip"2575},2576defaultArea: {2577attrs: {2578fill: "#343434",2579stroke: "#5d5d5d",2580"stroke-width": 1,2581"stroke-linejoin": "round"2582},2583attrsHover: {2584fill: "#f38a03",2585animDuration: 3002586},2587text: {2588position: "inner",2589margin: 10,2590attrs: {2591"font-size": 15,2592fill: "#c7c7c7"2593},2594attrsHover: {2595fill: "#eaeaea",2596"animDuration": 3002597}2598},2599target: "_self",2600cssClass: "area"2601},2602defaultPlot: {2603type: "circle",2604size: 15,2605attrs: {2606fill: "#0088db",2607stroke: "#fff",2608"stroke-width": 0,2609"stroke-linejoin": "round"2610},2611attrsHover: {2612"stroke-width": 3,2613animDuration: 3002614},2615text: {2616position: "right",2617margin: 10,2618attrs: {2619"font-size": 15,2620fill: "#c7c7c7"2621},2622attrsHover: {2623fill: "#eaeaea",2624animDuration: 3002625}2626},2627target: "_self",2628cssClass: "plot"2629},2630defaultLink: {2631factor: 0.5,2632attrs: {2633stroke: "#0088db",2634"stroke-width": 22635},2636attrsHover: {2637animDuration: 3002638},2639text: {2640position: "inner",2641margin: 10,2642attrs: {2643"font-size": 15,2644fill: "#c7c7c7"2645},2646attrsHover: {2647fill: "#eaeaea",2648animDuration: 3002649}2650},2651target: "_self",2652cssClass: "link"2653},2654zoom: {2655enabled: false,2656minLevel: 0,2657maxLevel: 10,2658step: 0.25,2659mousewheel: true,2660touch: true,2661animDuration: 200,2662animEasing: "linear",2663buttons: {2664"reset": {2665cssClass: "zoomButton zoomReset",2666content: "•", // bullet sign2667title: "Reset zoom"2668},2669"in": {2670cssClass: "zoomButton zoomIn",2671content: "+",2672title: "Zoom in"2673},2674"out": {2675cssClass: "zoomButton zoomOut",2676content: "−", // minus sign2677title: "Zoom out"2678}2679}2680}2681},2682legend: {2683redrawOnResize: true,2684area: [],2685plot: []2686},2687areas: {},2688plots: {},2689links: {}2690},2691
2692// Default legends option2693legendDefaultOptions: {2694area: {2695cssClass: "areaLegend",2696display: true,2697marginLeft: 10,2698marginLeftTitle: 5,2699marginBottomTitle: 10,2700marginLeftLabel: 10,2701marginBottom: 10,2702titleAttrs: {2703"font-size": 16,2704fill: "#343434",2705"text-anchor": "start"2706},2707labelAttrs: {2708"font-size": 12,2709fill: "#343434",2710"text-anchor": "start"2711},2712labelAttrsHover: {2713fill: "#787878",2714animDuration: 3002715},2716hideElemsOnClick: {2717enabled: true,2718opacity: 0.2,2719animDuration: 3002720},2721slices: [],2722mode: "vertical"2723},2724plot: {2725cssClass: "plotLegend",2726display: true,2727marginLeft: 10,2728marginLeftTitle: 5,2729marginBottomTitle: 10,2730marginLeftLabel: 10,2731marginBottom: 10,2732titleAttrs: {2733"font-size": 16,2734fill: "#343434",2735"text-anchor": "start"2736},2737labelAttrs: {2738"font-size": 12,2739fill: "#343434",2740"text-anchor": "start"2741},2742labelAttrsHover: {2743fill: "#787878",2744animDuration: 3002745},2746hideElemsOnClick: {2747enabled: true,2748opacity: 0.2,2749animDuration: 3002750},2751slices: [],2752mode: "vertical"2753}2754}2755
2756};2757
2758// Mapael version number2759// Accessible as $.mapael.version2760Mapael.version = version;2761
2762// Extend jQuery with Mapael2763if ($[pluginName] === undefined) $[pluginName] = Mapael;2764
2765// Add jQuery DOM function2766$.fn[pluginName] = function (options) {2767// Call Mapael on each element2768return this.each(function () {2769// Avoid leaking problem on multiple instanciation by removing an old mapael object on a container2770if ($.data(this, pluginName)) {2771$.data(this, pluginName).destroy();2772}2773// Create Mapael and save it as jQuery data2774// This allow external access to Mapael using $(".mapcontainer").data("mapael")2775$.data(this, pluginName, new Mapael(this, options));2776});2777};2778
2779return Mapael;2780
2781}));2782