jquery.mapael.js 120 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782
  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) {
  15. if (typeof exports === 'object') {
  16. // CommonJS
  17. module.exports = factory(require('jquery'), require('raphael'), require('jquery-mousewheel'));
  18. } else if (typeof define === 'function' && define.amd) {
  19. // AMD. Register as an anonymous module.
  20. define(['jquery', 'raphael', 'mousewheel'], factory);
  21. } else {
  22. // Browser globals
  23. factory(jQuery, Raphael, jQuery.fn.mousewheel);
  24. }
  25. }(function ($, Raphael, mousewheel, undefined) {
  26. "use strict";
  27. // The plugin name (used on several places)
  28. var pluginName = "mapael";
  29. // Version number of jQuery Mapael. See http://semver.org/ for more information.
  30. var version = "2.2.0";
  31. /*
  32. * Mapael constructor
  33. * Init instance vars and call init()
  34. * @param container the DOM element on which to apply the plugin
  35. * @param options the complete options to use
  36. */
  37. var Mapael = function (container, options) {
  38. var self = this;
  39. // the global container (DOM element object)
  40. self.container = container;
  41. // the global container (jQuery object)
  42. self.$container = $(container);
  43. // the global options
  44. self.options = self.extendDefaultOptions(options);
  45. // zoom TimeOut handler (used to set and clear)
  46. self.zoomTO = 0;
  47. // zoom center coordinate (set at touchstart)
  48. self.zoomCenterX = 0;
  49. self.zoomCenterY = 0;
  50. // Zoom pinch (set at touchstart and touchmove)
  51. self.previousPinchDist = 0;
  52. // Zoom data
  53. self.zoomData = {
  54. zoomLevel: 0,
  55. zoomX: 0,
  56. zoomY: 0,
  57. panX: 0,
  58. panY: 0
  59. };
  60. self.currentViewBox = {
  61. x: 0, y: 0, w: 0, h: 0
  62. };
  63. // Panning: tell if panning action is in progress
  64. self.panning = false;
  65. // Animate view box
  66. self.zoomAnimID = null; // Interval handler (used to set and clear)
  67. self.zoomAnimStartTime = null; // Animation start time
  68. self.zoomAnimCVBTarget = null; // Current ViewBox target
  69. // Map subcontainer jQuery object
  70. self.$map = $("." + self.options.map.cssClass, self.container);
  71. // Save initial HTML content (used by destroy method)
  72. self.initialMapHTMLContent = self.$map.html();
  73. // The tooltip jQuery object
  74. self.$tooltip = {};
  75. // The paper Raphael object
  76. self.paper = {};
  77. // The areas object list
  78. self.areas = {};
  79. // The plots object list
  80. self.plots = {};
  81. // The links object list
  82. self.links = {};
  83. // The legends list
  84. self.legends = {};
  85. // The map configuration object (taken from map file)
  86. self.mapConf = {};
  87. // Holds all custom event handlers
  88. self.customEventHandlers = {};
  89. // Let's start the initialization
  90. self.init();
  91. };
  92. /*
  93. * Mapael Prototype
  94. * Defines all methods and properties needed by Mapael
  95. * Each mapael object inherits their properties and methods from this prototype
  96. */
  97. Mapael.prototype = {
  98. /* Filtering TimeOut value in ms
  99. * Used for mouseover trigger over elements */
  100. MouseOverFilteringTO: 120,
  101. /* Filtering TimeOut value in ms
  102. * Used for afterPanning trigger when panning */
  103. panningFilteringTO: 150,
  104. /* Filtering TimeOut value in ms
  105. * Used for mouseup/touchend trigger when panning */
  106. panningEndFilteringTO: 50,
  107. /* Filtering TimeOut value in ms
  108. * Used for afterZoom trigger when zooming */
  109. zoomFilteringTO: 150,
  110. /* Filtering TimeOut value in ms
  111. * Used for when resizing window */
  112. resizeFilteringTO: 150,
  113. /*
  114. * Initialize the plugin
  115. * Called by the constructor
  116. */
  117. init: function () {
  118. var self = this;
  119. // Init check for class existence
  120. if (self.options.map.cssClass === "" || $("." + self.options.map.cssClass, self.container).length === 0) {
  121. throw new Error("The map class `" + self.options.map.cssClass + "` doesn't exists");
  122. }
  123. // Create the tooltip container
  124. self.$tooltip = $("<div>").addClass(self.options.map.tooltip.cssClass).css("display", "none");
  125. // Get the map container, empty it then append tooltip
  126. self.$map.empty().append(self.$tooltip);
  127. // Get the map from $.mapael or $.fn.mapael (backward compatibility)
  128. if ($[pluginName] && $[pluginName].maps && $[pluginName].maps[self.options.map.name]) {
  129. // Mapael version >= 2.x
  130. self.mapConf = $[pluginName].maps[self.options.map.name];
  131. } else if ($.fn[pluginName] && $.fn[pluginName].maps && $.fn[pluginName].maps[self.options.map.name]) {
  132. // Mapael version <= 1.x - DEPRECATED
  133. self.mapConf = $.fn[pluginName].maps[self.options.map.name];
  134. if (window.console && window.console.warn) {
  135. window.console.warn("Extending $.fn.mapael is deprecated (map '" + self.options.map.name + "')");
  136. }
  137. } else {
  138. throw new Error("Unknown map '" + self.options.map.name + "'");
  139. }
  140. // Create Raphael paper
  141. self.paper = new Raphael(self.$map[0], self.mapConf.width, self.mapConf.height);
  142. // issue #135: Check for Raphael bug on text element boundaries
  143. if (self.isRaphaelBBoxBugPresent() === true) {
  144. self.destroy();
  145. throw new Error("Can't get boundary box for text (is your container hidden? See #135)");
  146. }
  147. // add plugin class name on element
  148. self.$container.addClass(pluginName);
  149. if (self.options.map.tooltip.css) self.$tooltip.css(self.options.map.tooltip.css);
  150. self.setViewBox(0, 0, self.mapConf.width, self.mapConf.height);
  151. // Handle map size
  152. if (self.options.map.width) {
  153. // NOT responsive: map has a fixed width
  154. self.paper.setSize(self.options.map.width, self.mapConf.height * (self.options.map.width / self.mapConf.width));
  155. } else {
  156. // Responsive: handle resizing of the map
  157. self.initResponsiveSize();
  158. }
  159. // Draw map areas
  160. $.each(self.mapConf.elems, function (id) {
  161. // Init area object
  162. self.areas[id] = {};
  163. // Set area options
  164. self.areas[id].options = self.getElemOptions(
  165. self.options.map.defaultArea,
  166. (self.options.areas[id] ? self.options.areas[id] : {}),
  167. self.options.legend.area
  168. );
  169. // draw area
  170. self.areas[id].mapElem = self.paper.path(self.mapConf.elems[id]);
  171. });
  172. // Hook that allows to add custom processing on the map
  173. if (self.options.map.beforeInit) self.options.map.beforeInit(self.$container, self.paper, self.options);
  174. // Init map areas in a second loop
  175. // Allows text to be added after ALL areas and prevent them from being hidden
  176. $.each(self.mapConf.elems, function (id) {
  177. self.initElem(id, 'area', self.areas[id]);
  178. });
  179. // Draw links
  180. self.links = self.drawLinksCollection(self.options.links);
  181. // Draw plots
  182. $.each(self.options.plots, function (id) {
  183. self.plots[id] = self.drawPlot(id);
  184. });
  185. // Attach zoom event
  186. self.$container.on("zoom." + pluginName, function (e, zoomOptions) {
  187. self.onZoomEvent(e, zoomOptions);
  188. });
  189. if (self.options.map.zoom.enabled) {
  190. // Enable zoom
  191. self.initZoom(self.mapConf.width, self.mapConf.height, self.options.map.zoom);
  192. }
  193. // Set initial zoom
  194. if (self.options.map.zoom.init !== undefined) {
  195. if (self.options.map.zoom.init.animDuration === undefined) {
  196. self.options.map.zoom.init.animDuration = 0;
  197. }
  198. self.$container.trigger("zoom", self.options.map.zoom.init);
  199. }
  200. // Create the legends for areas
  201. self.createLegends("area", self.areas, 1);
  202. // Create the legends for plots taking into account the scale of the map
  203. self.createLegends("plot", self.plots, self.paper.width / self.mapConf.width);
  204. // Attach update event
  205. self.$container.on("update." + pluginName, function (e, opt) {
  206. self.onUpdateEvent(e, opt);
  207. });
  208. // Attach showElementsInRange event
  209. self.$container.on("showElementsInRange." + pluginName, function (e, opt) {
  210. self.onShowElementsInRange(e, opt);
  211. });
  212. // Attach delegated events
  213. self.initDelegatedMapEvents();
  214. // Attach delegated custom events
  215. self.initDelegatedCustomEvents();
  216. // Hook that allows to add custom processing on the map
  217. if (self.options.map.afterInit) self.options.map.afterInit(self.$container, self.paper, self.areas, self.plots, self.options);
  218. $(self.paper.desc).append(" and Mapael " + self.version + " (https://www.vincentbroute.fr/mapael/)");
  219. },
  220. /*
  221. * Destroy mapael
  222. * This function effectively detach mapael from the container
  223. * - Set the container back to the way it was before mapael instanciation
  224. * - Remove all data associated to it (memory can then be free'ed by browser)
  225. *
  226. * This method can be call directly by user:
  227. * $(".mapcontainer").data("mapael").destroy();
  228. *
  229. * This method is also automatically called if the user try to call mapael
  230. * on a container already containing a mapael instance
  231. */
  232. destroy: function () {
  233. var self = this;
  234. // Detach all event listeners attached to the container
  235. self.$container.off("." + pluginName);
  236. self.$map.off("." + pluginName);
  237. // Detach the global resize event handler
  238. if (self.onResizeEvent) $(window).off("resize." + pluginName, self.onResizeEvent);
  239. // Empty the container (this will also detach all event listeners)
  240. self.$map.empty();
  241. // Replace initial HTML content
  242. self.$map.html(self.initialMapHTMLContent);
  243. // Empty legend containers and replace initial HTML content
  244. $.each(self.legends, function(legendType) {
  245. $.each(self.legends[legendType], function(legendIndex) {
  246. var legend = self.legends[legendType][legendIndex];
  247. legend.container.empty();
  248. legend.container.html(legend.initialHTMLContent);
  249. });
  250. });
  251. // Remove mapael class
  252. self.$container.removeClass(pluginName);
  253. // Remove the data
  254. self.$container.removeData(pluginName);
  255. // Remove all internal reference
  256. self.container = undefined;
  257. self.$container = undefined;
  258. self.options = undefined;
  259. self.paper = undefined;
  260. self.$map = undefined;
  261. self.$tooltip = undefined;
  262. self.mapConf = undefined;
  263. self.areas = undefined;
  264. self.plots = undefined;
  265. self.links = undefined;
  266. self.customEventHandlers = undefined;
  267. },
  268. initResponsiveSize: function () {
  269. var self = this;
  270. var resizeTO = null;
  271. // Function that actually handle the resizing
  272. var handleResize = function(isInit) {
  273. var containerWidth = self.$map.width();
  274. if (self.paper.width !== containerWidth) {
  275. var newScale = containerWidth / self.mapConf.width;
  276. // Set new size
  277. self.paper.setSize(containerWidth, self.mapConf.height * newScale);
  278. // Create plots legend again to take into account the new scale
  279. // Do not do this on init (it will be done later)
  280. if (isInit !== true && self.options.legend.redrawOnResize) {
  281. self.createLegends("plot", self.plots, newScale);
  282. }
  283. }
  284. };
  285. self.onResizeEvent = function() {
  286. // Clear any previous setTimeout (avoid too much triggering)
  287. clearTimeout(resizeTO);
  288. // setTimeout to wait for the user to finish its resizing
  289. resizeTO = setTimeout(function () {
  290. handleResize();
  291. }, self.resizeFilteringTO);
  292. };
  293. // Attach resize handler
  294. $(window).on("resize." + pluginName, self.onResizeEvent);
  295. // Call once
  296. handleResize(true);
  297. },
  298. /*
  299. * Extend the user option with the default one
  300. * @param options the user options
  301. * @return new options object
  302. */
  303. extendDefaultOptions: function (options) {
  304. // Extend default options with user options
  305. options = $.extend(true, {}, Mapael.prototype.defaultOptions, options);
  306. // Extend legend default options
  307. $.each(['area', 'plot'], function (key, type) {
  308. if ($.isArray(options.legend[type])) {
  309. for (var i = 0; i < options.legend[type].length; ++i)
  310. options.legend[type][i] = $.extend(true, {}, Mapael.prototype.legendDefaultOptions[type], options.legend[type][i]);
  311. } else {
  312. options.legend[type] = $.extend(true, {}, Mapael.prototype.legendDefaultOptions[type], options.legend[type]);
  313. }
  314. });
  315. return options;
  316. },
  317. /*
  318. * Init all delegated events for the whole map:
  319. * mouseover
  320. * mousemove
  321. * mouseout
  322. */
  323. initDelegatedMapEvents: function() {
  324. var self = this;
  325. // Mapping between data-type value and the corresponding elements array
  326. // Note: legend-elem and legend-label are not in this table because
  327. // they need a special processing
  328. var dataTypeToElementMapping = {
  329. 'area' : self.areas,
  330. 'area-text' : self.areas,
  331. 'plot' : self.plots,
  332. 'plot-text' : self.plots,
  333. 'link' : self.links,
  334. 'link-text' : self.links
  335. };
  336. /* Attach mouseover event delegation
  337. * Note: we filter the event with a timeout to reduce the firing when the mouse moves quickly
  338. */
  339. var mapMouseOverTimeoutID;
  340. self.$container.on("mouseover." + pluginName, "[data-id]", function () {
  341. var elem = this;
  342. clearTimeout(mapMouseOverTimeoutID);
  343. mapMouseOverTimeoutID = setTimeout(function() {
  344. var $elem = $(elem);
  345. var id = $elem.attr('data-id');
  346. var type = $elem.attr('data-type');
  347. if (dataTypeToElementMapping[type] !== undefined) {
  348. self.elemEnter(dataTypeToElementMapping[type][id]);
  349. } else if (type === 'legend-elem' || type === 'legend-label') {
  350. var legendIndex = $elem.attr('data-legend-id');
  351. var legendType = $elem.attr('data-legend-type');
  352. self.elemEnter(self.legends[legendType][legendIndex].elems[id]);
  353. }
  354. }, self.MouseOverFilteringTO);
  355. });
  356. /* Attach mousemove event delegation
  357. * Note: timeout filtering is small to update the Tooltip position fast
  358. */
  359. var mapMouseMoveTimeoutID;
  360. self.$container.on("mousemove." + pluginName, "[data-id]", function (event) {
  361. var elem = this;
  362. clearTimeout(mapMouseMoveTimeoutID);
  363. mapMouseMoveTimeoutID = setTimeout(function() {
  364. var $elem = $(elem);
  365. var id = $elem.attr('data-id');
  366. var type = $elem.attr('data-type');
  367. if (dataTypeToElementMapping[type] !== undefined) {
  368. self.elemHover(dataTypeToElementMapping[type][id], event);
  369. } else if (type === 'legend-elem' || type === 'legend-label') {
  370. /* Nothing to do */
  371. }
  372. }, 0);
  373. });
  374. /* Attach mouseout event delegation
  375. * Note: we don't perform any timeout filtering to clear & reset elem ASAP
  376. * Otherwise an element may be stuck in 'hover' state (which is NOT good)
  377. */
  378. self.$container.on("mouseout." + pluginName, "[data-id]", function () {
  379. var elem = this;
  380. // Clear any
  381. clearTimeout(mapMouseOverTimeoutID);
  382. clearTimeout(mapMouseMoveTimeoutID);
  383. var $elem = $(elem);
  384. var id = $elem.attr('data-id');
  385. var type = $elem.attr('data-type');
  386. if (dataTypeToElementMapping[type] !== undefined) {
  387. self.elemOut(dataTypeToElementMapping[type][id]);
  388. } else if (type === 'legend-elem' || type === 'legend-label') {
  389. var legendIndex = $elem.attr('data-legend-id');
  390. var legendType = $elem.attr('data-legend-type');
  391. self.elemOut(self.legends[legendType][legendIndex].elems[id]);
  392. }
  393. });
  394. /* Attach click event delegation
  395. * Note: we filter the event with a timeout to avoid double click
  396. */
  397. self.$container.on("click." + pluginName, "[data-id]", function (evt, opts) {
  398. var $elem = $(this);
  399. var id = $elem.attr('data-id');
  400. var type = $elem.attr('data-type');
  401. if (dataTypeToElementMapping[type] !== undefined) {
  402. self.elemClick(dataTypeToElementMapping[type][id]);
  403. } else if (type === 'legend-elem' || type === 'legend-label') {
  404. var legendIndex = $elem.attr('data-legend-id');
  405. var legendType = $elem.attr('data-legend-type');
  406. self.handleClickOnLegendElem(self.legends[legendType][legendIndex].elems[id], id, legendIndex, legendType, opts);
  407. }
  408. });
  409. },
  410. /*
  411. * Init all delegated custom events
  412. */
  413. initDelegatedCustomEvents: function() {
  414. var self = this;
  415. $.each(self.customEventHandlers, function(eventName) {
  416. // Namespace the custom event
  417. // This allow to easily unbound only custom events and not regular ones
  418. var fullEventName = eventName + '.' + pluginName + ".custom";
  419. self.$container.off(fullEventName).on(fullEventName, "[data-id]", function (e) {
  420. var $elem = $(this);
  421. var id = $elem.attr('data-id');
  422. var type = $elem.attr('data-type').replace('-text', '');
  423. if (!self.panning &&
  424. self.customEventHandlers[eventName][type] !== undefined &&
  425. self.customEventHandlers[eventName][type][id] !== undefined)
  426. {
  427. // Get back related elem
  428. var elem = self.customEventHandlers[eventName][type][id];
  429. // Run callback provided by user
  430. elem.options.eventHandlers[eventName](e, id, elem.mapElem, elem.textElem, elem.options);
  431. }
  432. });
  433. });
  434. },
  435. /*
  436. * Init the element "elem" on the map (drawing text, setting attributes, events, tooltip, ...)
  437. *
  438. * @param id the id of the element
  439. * @param type the type of the element (area, plot, link)
  440. * @param elem object the element object (with mapElem), it will be updated
  441. */
  442. initElem: function (id, type, elem) {
  443. var self = this;
  444. var $mapElem = $(elem.mapElem.node);
  445. // If an HTML link exists for this element, add cursor attributes
  446. if (elem.options.href) {
  447. elem.options.attrs.cursor = "pointer";
  448. if (elem.options.text) elem.options.text.attrs.cursor = "pointer";
  449. }
  450. // Set SVG attributes to map element
  451. elem.mapElem.attr(elem.options.attrs);
  452. // Set DOM attributes to map element
  453. $mapElem.attr({
  454. "data-id": id,
  455. "data-type": type
  456. });
  457. if (elem.options.cssClass !== undefined) {
  458. $mapElem.addClass(elem.options.cssClass);
  459. }
  460. // Init the label related to the element
  461. if (elem.options.text && elem.options.text.content !== undefined) {
  462. // Set a text label in the area
  463. var textPosition = self.getTextPosition(elem.mapElem.getBBox(), elem.options.text.position, elem.options.text.margin);
  464. elem.options.text.attrs.text = elem.options.text.content;
  465. elem.options.text.attrs.x = textPosition.x;
  466. elem.options.text.attrs.y = textPosition.y;
  467. elem.options.text.attrs['text-anchor'] = textPosition.textAnchor;
  468. // Draw text
  469. elem.textElem = self.paper.text(textPosition.x, textPosition.y, elem.options.text.content);
  470. // Apply SVG attributes to text element
  471. elem.textElem.attr(elem.options.text.attrs);
  472. // Apply DOM attributes
  473. $(elem.textElem.node).attr({
  474. "data-id": id,
  475. "data-type": type + '-text'
  476. });
  477. }
  478. // Set user event handlers
  479. if (elem.options.eventHandlers) self.setEventHandlers(id, type, elem);
  480. // Set hover option for mapElem
  481. self.setHoverOptions(elem.mapElem, elem.options.attrs, elem.options.attrsHover);
  482. // Set hover option for textElem
  483. if (elem.textElem) self.setHoverOptions(elem.textElem, elem.options.text.attrs, elem.options.text.attrsHover);
  484. },
  485. /*
  486. * Init zoom and panning for the map
  487. * @param mapWidth
  488. * @param mapHeight
  489. * @param zoomOptions
  490. */
  491. initZoom: function (mapWidth, mapHeight, zoomOptions) {
  492. var self = this;
  493. var mousedown = false;
  494. var previousX = 0;
  495. var previousY = 0;
  496. var fnZoomButtons = {
  497. "reset": function () {
  498. self.$container.trigger("zoom", {"level": 0});
  499. },
  500. "in": function () {
  501. self.$container.trigger("zoom", {"level": "+1"});
  502. },
  503. "out": function () {
  504. self.$container.trigger("zoom", {"level": -1});
  505. }
  506. };
  507. // init Zoom data
  508. $.extend(self.zoomData, {
  509. zoomLevel: 0,
  510. panX: 0,
  511. panY: 0
  512. });
  513. // init zoom buttons
  514. $.each(zoomOptions.buttons, function(type, opt) {
  515. if (fnZoomButtons[type] === undefined) throw new Error("Unknown zoom button '" + type + "'");
  516. // Create div with classes, contents and title (for tooltip)
  517. var $button = $("<div>").addClass(opt.cssClass)
  518. .html(opt.content)
  519. .attr("title", opt.title);
  520. // Assign click event
  521. $button.on("click." + pluginName, fnZoomButtons[type]);
  522. // Append to map
  523. self.$map.append($button);
  524. });
  525. // Update the zoom level of the map on mousewheel
  526. if (self.options.map.zoom.mousewheel) {
  527. self.$map.on("mousewheel." + pluginName, function (e) {
  528. var zoomLevel = (e.deltaY > 0) ? 1 : -1;
  529. var coord = self.mapPagePositionToXY(e.pageX, e.pageY);
  530. self.$container.trigger("zoom", {
  531. "fixedCenter": true,
  532. "level": self.zoomData.zoomLevel + zoomLevel,
  533. "x": coord.x,
  534. "y": coord.y
  535. });
  536. e.preventDefault();
  537. });
  538. }
  539. // Update the zoom level of the map on touch pinch
  540. if (self.options.map.zoom.touch) {
  541. self.$map.on("touchstart." + pluginName, function (e) {
  542. if (e.originalEvent.touches.length === 2) {
  543. self.zoomCenterX = (e.originalEvent.touches[0].pageX + e.originalEvent.touches[1].pageX) / 2;
  544. self.zoomCenterY = (e.originalEvent.touches[0].pageY + e.originalEvent.touches[1].pageY) / 2;
  545. self.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));
  546. }
  547. });
  548. self.$map.on("touchmove." + pluginName, function (e) {
  549. var pinchDist = 0;
  550. var zoomLevel = 0;
  551. if (e.originalEvent.touches.length === 2) {
  552. pinchDist = 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));
  553. if (Math.abs(pinchDist - self.previousPinchDist) > 15) {
  554. var coord = self.mapPagePositionToXY(self.zoomCenterX, self.zoomCenterY);
  555. zoomLevel = (pinchDist - self.previousPinchDist) / Math.abs(pinchDist - self.previousPinchDist);
  556. self.$container.trigger("zoom", {
  557. "fixedCenter": true,
  558. "level": self.zoomData.zoomLevel + zoomLevel,
  559. "x": coord.x,
  560. "y": coord.y
  561. });
  562. self.previousPinchDist = pinchDist;
  563. }
  564. return false;
  565. }
  566. });
  567. }
  568. // When the user drag the map, prevent to move the clicked element instead of dragging the map (behaviour seen with Firefox)
  569. self.$map.on("dragstart", function() {
  570. return false;
  571. });
  572. // Panning
  573. var panningMouseUpTO = null;
  574. var panningMouseMoveTO = null;
  575. $("body").on("mouseup." + pluginName + (zoomOptions.touch ? " touchend." + pluginName : ""), function () {
  576. mousedown = false;
  577. clearTimeout(panningMouseUpTO);
  578. clearTimeout(panningMouseMoveTO);
  579. panningMouseUpTO = setTimeout(function () {
  580. self.panning = false;
  581. }, self.panningEndFilteringTO);
  582. });
  583. self.$map.on("mousedown." + pluginName + (zoomOptions.touch ? " touchstart." + pluginName : ""), function (e) {
  584. clearTimeout(panningMouseUpTO);
  585. clearTimeout(panningMouseMoveTO);
  586. if (e.pageX !== undefined) {
  587. mousedown = true;
  588. previousX = e.pageX;
  589. previousY = e.pageY;
  590. } else {
  591. if (e.originalEvent.touches.length === 1) {
  592. mousedown = true;
  593. previousX = e.originalEvent.touches[0].pageX;
  594. previousY = e.originalEvent.touches[0].pageY;
  595. }
  596. }
  597. }).on("mousemove." + pluginName + (zoomOptions.touch ? " touchmove." + pluginName : ""), function (e) {
  598. var currentLevel = self.zoomData.zoomLevel;
  599. var pageX = 0;
  600. var pageY = 0;
  601. clearTimeout(panningMouseUpTO);
  602. clearTimeout(panningMouseMoveTO);
  603. if (e.pageX !== undefined) {
  604. pageX = e.pageX;
  605. pageY = e.pageY;
  606. } else {
  607. if (e.originalEvent.touches.length === 1) {
  608. pageX = e.originalEvent.touches[0].pageX;
  609. pageY = e.originalEvent.touches[0].pageY;
  610. } else {
  611. mousedown = false;
  612. }
  613. }
  614. if (mousedown && currentLevel !== 0) {
  615. var offsetX = (previousX - pageX) / (1 + (currentLevel * zoomOptions.step)) * (mapWidth / self.paper.width);
  616. var offsetY = (previousY - pageY) / (1 + (currentLevel * zoomOptions.step)) * (mapHeight / self.paper.height);
  617. var panX = Math.min(Math.max(0, self.currentViewBox.x + offsetX), (mapWidth - self.currentViewBox.w));
  618. var panY = Math.min(Math.max(0, self.currentViewBox.y + offsetY), (mapHeight - self.currentViewBox.h));
  619. if (Math.abs(offsetX) > 5 || Math.abs(offsetY) > 5) {
  620. $.extend(self.zoomData, {
  621. panX: panX,
  622. panY: panY,
  623. zoomX: panX + self.currentViewBox.w / 2,
  624. zoomY: panY + self.currentViewBox.h / 2
  625. });
  626. self.setViewBox(panX, panY, self.currentViewBox.w, self.currentViewBox.h);
  627. panningMouseMoveTO = setTimeout(function () {
  628. self.$map.trigger("afterPanning", {
  629. x1: panX,
  630. y1: panY,
  631. x2: (panX + self.currentViewBox.w),
  632. y2: (panY + self.currentViewBox.h)
  633. });
  634. }, self.panningFilteringTO);
  635. previousX = pageX;
  636. previousY = pageY;
  637. self.panning = true;
  638. }
  639. return false;
  640. }
  641. });
  642. },
  643. /*
  644. * Map a mouse position to a map position
  645. * Transformation principle:
  646. * ** start with (pageX, pageY) absolute mouse coordinate
  647. * - Apply translation: take into accounts the map offset in the page
  648. * ** from this point, we have relative mouse coordinate
  649. * - Apply homothetic transformation: take into accounts initial factor of map sizing (fullWidth / actualWidth)
  650. * - Apply homothetic transformation: take into accounts the zoom factor
  651. * ** from this point, we have relative map coordinate
  652. * - Apply translation: take into accounts the current panning of the map
  653. * ** from this point, we have absolute map coordinate
  654. * @param pageX: mouse client coordinate on X
  655. * @param pageY: mouse client coordinate on Y
  656. * @return map coordinate {x, y}
  657. */
  658. mapPagePositionToXY: function(pageX, pageY) {
  659. var self = this;
  660. var offset = self.$map.offset();
  661. var initFactor = (self.options.map.width) ? (self.mapConf.width / self.options.map.width) : (self.mapConf.width / self.$map.width());
  662. var zoomFactor = 1 / (1 + (self.zoomData.zoomLevel * self.options.map.zoom.step));
  663. return {
  664. x: (zoomFactor * initFactor * (pageX - offset.left)) + self.zoomData.panX,
  665. y: (zoomFactor * initFactor * (pageY - offset.top)) + self.zoomData.panY
  666. };
  667. },
  668. /*
  669. * Zoom on the map
  670. *
  671. * zoomOptions.animDuration zoom duration
  672. *
  673. * zoomOptions.level level of the zoom between minLevel and maxLevel (absolute number, or relative string +1 or -1)
  674. * zoomOptions.fixedCenter set to true in order to preserve the position of x,y in the canvas when zoomed
  675. *
  676. * zoomOptions.x x coordinate of the point to focus on
  677. * zoomOptions.y y coordinate of the point to focus on
  678. * - OR -
  679. * zoomOptions.latitude latitude of the point to focus on
  680. * zoomOptions.longitude longitude of the point to focus on
  681. * - OR -
  682. * zoomOptions.plot plot ID to focus on
  683. * - OR -
  684. * zoomOptions.area area ID to focus on
  685. * zoomOptions.areaMargin margin (in pixels) around the area
  686. *
  687. * If an area ID is specified, the algorithm will override the zoom level to focus on the area
  688. * but it may be limited by the min/max zoom level limits set at initialization.
  689. *
  690. * If no coordinates are specified, the zoom will be focused on the center of the current view box
  691. *
  692. */
  693. onZoomEvent: function (e, zoomOptions) {
  694. var self = this;
  695. // new Top/Left corner coordinates
  696. var panX;
  697. var panY;
  698. // new Width/Height viewbox size
  699. var panWidth;
  700. var panHeight;
  701. // Zoom level in absolute scale (from 0 to max, by step of 1)
  702. var zoomLevel = self.zoomData.zoomLevel;
  703. // Relative zoom level (from 1 to max, by step of 0.25 (default))
  704. var previousRelativeZoomLevel = 1 + self.zoomData.zoomLevel * self.options.map.zoom.step;
  705. var relativeZoomLevel;
  706. var animDuration = (zoomOptions.animDuration !== undefined) ? zoomOptions.animDuration : self.options.map.zoom.animDuration;
  707. if (zoomOptions.area !== undefined) {
  708. /* An area is given
  709. * We will define x/y coordinate AND a new zoom level to fill the area
  710. */
  711. if (self.areas[zoomOptions.area] === undefined) throw new Error("Unknown area '" + zoomOptions.area + "'");
  712. var areaMargin = (zoomOptions.areaMargin !== undefined) ? zoomOptions.areaMargin : 10;
  713. var areaBBox = self.areas[zoomOptions.area].mapElem.getBBox();
  714. var areaFullWidth = areaBBox.width + 2 * areaMargin;
  715. var areaFullHeight = areaBBox.height + 2 * areaMargin;
  716. // Compute new x/y focus point (center of area)
  717. zoomOptions.x = areaBBox.cx;
  718. zoomOptions.y = areaBBox.cy;
  719. // Compute a new absolute zoomLevel value (inverse of relative -> absolute)
  720. // Take the min between zoomLevel on width vs. height to be able to see the whole area
  721. zoomLevel = Math.min(Math.floor((self.mapConf.width / areaFullWidth - 1) / self.options.map.zoom.step),
  722. Math.floor((self.mapConf.height / areaFullHeight - 1) / self.options.map.zoom.step));
  723. } else {
  724. // Get user defined zoom level
  725. if (zoomOptions.level !== undefined) {
  726. if (typeof zoomOptions.level === "string") {
  727. // level is a string, either "n", "+n" or "-n"
  728. if ((zoomOptions.level.slice(0, 1) === '+') || (zoomOptions.level.slice(0, 1) === '-')) {
  729. // zoomLevel is relative
  730. zoomLevel = self.zoomData.zoomLevel + parseInt(zoomOptions.level, 10);
  731. } else {
  732. // zoomLevel is absolute
  733. zoomLevel = parseInt(zoomOptions.level, 10);
  734. }
  735. } else {
  736. // level is integer
  737. if (zoomOptions.level < 0) {
  738. // zoomLevel is relative
  739. zoomLevel = self.zoomData.zoomLevel + zoomOptions.level;
  740. } else {
  741. // zoomLevel is absolute
  742. zoomLevel = zoomOptions.level;
  743. }
  744. }
  745. }
  746. if (zoomOptions.plot !== undefined) {
  747. if (self.plots[zoomOptions.plot] === undefined) throw new Error("Unknown plot '" + zoomOptions.plot + "'");
  748. zoomOptions.x = self.plots[zoomOptions.plot].coords.x;
  749. zoomOptions.y = self.plots[zoomOptions.plot].coords.y;
  750. } else {
  751. if (zoomOptions.latitude !== undefined && zoomOptions.longitude !== undefined) {
  752. var coords = self.mapConf.getCoords(zoomOptions.latitude, zoomOptions.longitude);
  753. zoomOptions.x = coords.x;
  754. zoomOptions.y = coords.y;
  755. }
  756. if (zoomOptions.x === undefined) {
  757. zoomOptions.x = self.currentViewBox.x + self.currentViewBox.w / 2;
  758. }
  759. if (zoomOptions.y === undefined) {
  760. zoomOptions.y = self.currentViewBox.y + self.currentViewBox.h / 2;
  761. }
  762. }
  763. }
  764. // Make sure we stay in the zoom level boundaries
  765. zoomLevel = Math.min(Math.max(zoomLevel, self.options.map.zoom.minLevel), self.options.map.zoom.maxLevel);
  766. // Compute relative zoom level
  767. relativeZoomLevel = 1 + zoomLevel * self.options.map.zoom.step;
  768. // Compute panWidth / panHeight
  769. panWidth = self.mapConf.width / relativeZoomLevel;
  770. panHeight = self.mapConf.height / relativeZoomLevel;
  771. if (zoomLevel === 0) {
  772. panX = 0;
  773. panY = 0;
  774. } else {
  775. if (zoomOptions.fixedCenter !== undefined && zoomOptions.fixedCenter === true) {
  776. panX = self.zoomData.panX + ((zoomOptions.x - self.zoomData.panX) * (relativeZoomLevel - previousRelativeZoomLevel)) / relativeZoomLevel;
  777. panY = self.zoomData.panY + ((zoomOptions.y - self.zoomData.panY) * (relativeZoomLevel - previousRelativeZoomLevel)) / relativeZoomLevel;
  778. } else {
  779. panX = zoomOptions.x - panWidth / 2;
  780. panY = zoomOptions.y - panHeight / 2;
  781. }
  782. // Make sure we stay in the map boundaries
  783. panX = Math.min(Math.max(0, panX), self.mapConf.width - panWidth);
  784. panY = Math.min(Math.max(0, panY), self.mapConf.height - panHeight);
  785. }
  786. // Update zoom level of the map
  787. if (relativeZoomLevel === previousRelativeZoomLevel && panX === self.zoomData.panX && panY === self.zoomData.panY) return;
  788. if (animDuration > 0) {
  789. self.animateViewBox(panX, panY, panWidth, panHeight, animDuration, self.options.map.zoom.animEasing);
  790. } else {
  791. self.setViewBox(panX, panY, panWidth, panHeight);
  792. clearTimeout(self.zoomTO);
  793. self.zoomTO = setTimeout(function () {
  794. self.$map.trigger("afterZoom", {
  795. x1: panX,
  796. y1: panY,
  797. x2: panX + panWidth,
  798. y2: panY + panHeight
  799. });
  800. }, self.zoomFilteringTO);
  801. }
  802. $.extend(self.zoomData, {
  803. zoomLevel: zoomLevel,
  804. panX: panX,
  805. panY: panY,
  806. zoomX: panX + panWidth / 2,
  807. zoomY: panY + panHeight / 2
  808. });
  809. },
  810. /*
  811. * Show some element in range defined by user
  812. * Triggered by user $(".mapcontainer").trigger("showElementsInRange", [opt]);
  813. *
  814. * @param opt the options
  815. * opt.hiddenOpacity opacity for hidden element (default = 0.3)
  816. * opt.animDuration animation duration in ms (default = 0)
  817. * opt.afterShowRange callback
  818. * opt.ranges the range to show:
  819. * Example:
  820. * opt.ranges = {
  821. * 'plot' : {
  822. * 0 : { // valueIndex
  823. * 'min': 1000,
  824. * 'max': 1200
  825. * },
  826. * 1 : { // valueIndex
  827. * 'min': 10,
  828. * 'max': 12
  829. * }
  830. * },
  831. * 'area' : {
  832. * {'min': 10, 'max': 20} // No valueIndex, only an object, use 0 as valueIndex (easy case)
  833. * }
  834. * }
  835. */
  836. onShowElementsInRange: function(e, opt) {
  837. var self = this;
  838. // set animDuration to default if not defined
  839. if (opt.animDuration === undefined) {
  840. opt.animDuration = 0;
  841. }
  842. // set hiddenOpacity to default if not defined
  843. if (opt.hiddenOpacity === undefined) {
  844. opt.hiddenOpacity = 0.3;
  845. }
  846. // handle area
  847. if (opt.ranges && opt.ranges.area) {
  848. self.showElemByRange(opt.ranges.area, self.areas, opt.hiddenOpacity, opt.animDuration);
  849. }
  850. // handle plot
  851. if (opt.ranges && opt.ranges.plot) {
  852. self.showElemByRange(opt.ranges.plot, self.plots, opt.hiddenOpacity, opt.animDuration);
  853. }
  854. // handle link
  855. if (opt.ranges && opt.ranges.link) {
  856. self.showElemByRange(opt.ranges.link, self.links, opt.hiddenOpacity, opt.animDuration);
  857. }
  858. // Call user callback
  859. if (opt.afterShowRange) opt.afterShowRange();
  860. },
  861. /*
  862. * Show some element in range
  863. * @param ranges: the ranges
  864. * @param elems: list of element on which to check against previous range
  865. * @hiddenOpacity: the opacity when hidden
  866. * @animDuration: the animation duration
  867. */
  868. showElemByRange: function(ranges, elems, hiddenOpacity, animDuration) {
  869. var self = this;
  870. // Hold the final opacity value for all elements consolidated after applying each ranges
  871. // This allow to set the opacity only once for each elements
  872. var elemsFinalOpacity = {};
  873. // set object with one valueIndex to 0 if we have directly the min/max
  874. if (ranges.min !== undefined || ranges.max !== undefined) {
  875. ranges = {0: ranges};
  876. }
  877. // Loop through each valueIndex
  878. $.each(ranges, function (valueIndex) {
  879. var range = ranges[valueIndex];
  880. // Check if user defined at least a min or max value
  881. if (range.min === undefined && range.max === undefined) {
  882. return true; // skip this iteration (each loop), goto next range
  883. }
  884. // Loop through each elements
  885. $.each(elems, function (id) {
  886. var elemValue = elems[id].options.value;
  887. // set value with one valueIndex to 0 if not object
  888. if (typeof elemValue !== "object") {
  889. elemValue = [elemValue];
  890. }
  891. // Check existence of this value index
  892. if (elemValue[valueIndex] === undefined) {
  893. return true; // skip this iteration (each loop), goto next element
  894. }
  895. // Check if in range
  896. if ((range.min !== undefined && elemValue[valueIndex] < range.min) ||
  897. (range.max !== undefined && elemValue[valueIndex] > range.max)) {
  898. // Element not in range
  899. elemsFinalOpacity[id] = hiddenOpacity;
  900. } else {
  901. // Element in range
  902. elemsFinalOpacity[id] = 1;
  903. }
  904. });
  905. });
  906. // Now that we looped through all ranges, we can really assign the final opacity
  907. $.each(elemsFinalOpacity, function (id) {
  908. self.setElementOpacity(elems[id], elemsFinalOpacity[id], animDuration);
  909. });
  910. },
  911. /*
  912. * Set element opacity
  913. * Handle elem.mapElem and elem.textElem
  914. * @param elem the element
  915. * @param opacity the opacity to apply
  916. * @param animDuration the animation duration to use
  917. */
  918. setElementOpacity: function(elem, opacity, animDuration) {
  919. var self = this;
  920. // Ensure no animation is running
  921. //elem.mapElem.stop();
  922. //if (elem.textElem) elem.textElem.stop();
  923. // If final opacity is not null, ensure element is shown before proceeding
  924. if (opacity > 0) {
  925. elem.mapElem.show();
  926. if (elem.textElem) elem.textElem.show();
  927. }
  928. self.animate(elem.mapElem, {"opacity": opacity}, animDuration, function () {
  929. // If final attribute is 0, hide
  930. if (opacity === 0) elem.mapElem.hide();
  931. });
  932. self.animate(elem.textElem, {"opacity": opacity}, animDuration, function () {
  933. // If final attribute is 0, hide
  934. if (opacity === 0) elem.textElem.hide();
  935. });
  936. },
  937. /*
  938. * Update the current map
  939. *
  940. * Refresh attributes and tooltips for areas and plots
  941. * @param opt option for the refresh :
  942. * opt.mapOptions: options to update for plots and areas
  943. * opt.replaceOptions: whether mapsOptions should entirely replace current map options, or just extend it
  944. * opt.opt.newPlots new plots to add to the map
  945. * opt.newLinks new links to add to the map
  946. * opt.deletePlotKeys plots to delete from the map (array, or "all" to remove all plots)
  947. * opt.deleteLinkKeys links to remove from the map (array, or "all" to remove all links)
  948. * opt.setLegendElemsState the state of legend elements to be set : show (default) or hide
  949. * opt.animDuration animation duration in ms (default = 0)
  950. * opt.afterUpdate hook that allows to add custom processing on the map
  951. */
  952. onUpdateEvent: function (e, opt) {
  953. var self = this;
  954. // Abort if opt is undefined
  955. if (typeof opt !== "object") return;
  956. var i = 0;
  957. var animDuration = (opt.animDuration) ? opt.animDuration : 0;
  958. // This function remove an element using animation (or not, depending on animDuration)
  959. // Used for deletePlotKeys and deleteLinkKeys
  960. var fnRemoveElement = function (elem) {
  961. self.animate(elem.mapElem, {"opacity": 0}, animDuration, function () {
  962. elem.mapElem.remove();
  963. });
  964. self.animate(elem.textElem, {"opacity": 0}, animDuration, function () {
  965. elem.textElem.remove();
  966. });
  967. };
  968. // This function show an element using animation
  969. // Used for newPlots and newLinks
  970. var fnShowElement = function (elem) {
  971. // Starts with hidden elements
  972. elem.mapElem.attr({opacity: 0});
  973. if (elem.textElem) elem.textElem.attr({opacity: 0});
  974. // Set final element opacity
  975. self.setElementOpacity(
  976. elem,
  977. (elem.mapElem.originalAttrs.opacity !== undefined) ? elem.mapElem.originalAttrs.opacity : 1,
  978. animDuration
  979. );
  980. };
  981. if (typeof opt.mapOptions === "object") {
  982. if (opt.replaceOptions === true) self.options = self.extendDefaultOptions(opt.mapOptions);
  983. else $.extend(true, self.options, opt.mapOptions);
  984. // IF we update areas, plots or legend, then reset all legend state to "show"
  985. if (opt.mapOptions.areas !== undefined || opt.mapOptions.plots !== undefined || opt.mapOptions.legend !== undefined) {
  986. $("[data-type='legend-elem']", self.$container).each(function (id, elem) {
  987. if ($(elem).attr('data-hidden') === "1") {
  988. // Toggle state of element by clicking
  989. $(elem).trigger("click", {hideOtherElems: false, animDuration: animDuration});
  990. }
  991. });
  992. }
  993. }
  994. // Delete plots by name if deletePlotKeys is array
  995. if (typeof opt.deletePlotKeys === "object") {
  996. for (; i < opt.deletePlotKeys.length; i++) {
  997. if (self.plots[opt.deletePlotKeys[i]] !== undefined) {
  998. fnRemoveElement(self.plots[opt.deletePlotKeys[i]]);
  999. delete self.plots[opt.deletePlotKeys[i]];
  1000. }
  1001. }
  1002. // Delete ALL plots if deletePlotKeys is set to "all"
  1003. } else if (opt.deletePlotKeys === "all") {
  1004. $.each(self.plots, function (id, elem) {
  1005. fnRemoveElement(elem);
  1006. });
  1007. // Empty plots object
  1008. self.plots = {};
  1009. }
  1010. // Delete links by name if deleteLinkKeys is array
  1011. if (typeof opt.deleteLinkKeys === "object") {
  1012. for (i = 0; i < opt.deleteLinkKeys.length; i++) {
  1013. if (self.links[opt.deleteLinkKeys[i]] !== undefined) {
  1014. fnRemoveElement(self.links[opt.deleteLinkKeys[i]]);
  1015. delete self.links[opt.deleteLinkKeys[i]];
  1016. }
  1017. }
  1018. // Delete ALL links if deleteLinkKeys is set to "all"
  1019. } else if (opt.deleteLinkKeys === "all") {
  1020. $.each(self.links, function (id, elem) {
  1021. fnRemoveElement(elem);
  1022. });
  1023. // Empty links object
  1024. self.links = {};
  1025. }
  1026. // New plots
  1027. if (typeof opt.newPlots === "object") {
  1028. $.each(opt.newPlots, function (id) {
  1029. if (self.plots[id] === undefined) {
  1030. self.options.plots[id] = opt.newPlots[id];
  1031. self.plots[id] = self.drawPlot(id);
  1032. if (animDuration > 0) {
  1033. fnShowElement(self.plots[id]);
  1034. }
  1035. }
  1036. });
  1037. }
  1038. // New links
  1039. if (typeof opt.newLinks === "object") {
  1040. var newLinks = self.drawLinksCollection(opt.newLinks);
  1041. $.extend(self.links, newLinks);
  1042. $.extend(self.options.links, opt.newLinks);
  1043. if (animDuration > 0) {
  1044. $.each(newLinks, function (id) {
  1045. fnShowElement(newLinks[id]);
  1046. });
  1047. }
  1048. }
  1049. // Update areas attributes and tooltips
  1050. $.each(self.areas, function (id) {
  1051. // Avoid updating unchanged elements
  1052. if ((typeof opt.mapOptions === "object" &&
  1053. (
  1054. (typeof opt.mapOptions.map === "object" && typeof opt.mapOptions.map.defaultArea === "object") ||
  1055. (typeof opt.mapOptions.areas === "object" && typeof opt.mapOptions.areas[id] === "object") ||
  1056. (typeof opt.mapOptions.legend === "object" && typeof opt.mapOptions.legend.area === "object")
  1057. )) || opt.replaceOptions === true
  1058. ) {
  1059. self.areas[id].options = self.getElemOptions(
  1060. self.options.map.defaultArea,
  1061. (self.options.areas[id] ? self.options.areas[id] : {}),
  1062. self.options.legend.area
  1063. );
  1064. self.updateElem(self.areas[id], animDuration);
  1065. }
  1066. });
  1067. // Update plots attributes and tooltips
  1068. $.each(self.plots, function (id) {
  1069. // Avoid updating unchanged elements
  1070. if ((typeof opt.mapOptions ==="object" &&
  1071. (
  1072. (typeof opt.mapOptions.map === "object" && typeof opt.mapOptions.map.defaultPlot === "object") ||
  1073. (typeof opt.mapOptions.plots === "object" && typeof opt.mapOptions.plots[id] === "object") ||
  1074. (typeof opt.mapOptions.legend === "object" && typeof opt.mapOptions.legend.plot === "object")
  1075. )) || opt.replaceOptions === true
  1076. ) {
  1077. self.plots[id].options = self.getElemOptions(
  1078. self.options.map.defaultPlot,
  1079. (self.options.plots[id] ? self.options.plots[id] : {}),
  1080. self.options.legend.plot
  1081. );
  1082. self.setPlotCoords(self.plots[id]);
  1083. self.setPlotAttributes(self.plots[id]);
  1084. self.updateElem(self.plots[id], animDuration);
  1085. }
  1086. });
  1087. // Update links attributes and tooltips
  1088. $.each(self.links, function (id) {
  1089. // Avoid updating unchanged elements
  1090. if ((typeof opt.mapOptions === "object" &&
  1091. (
  1092. (typeof opt.mapOptions.map === "object" && typeof opt.mapOptions.map.defaultLink === "object") ||
  1093. (typeof opt.mapOptions.links === "object" && typeof opt.mapOptions.links[id] === "object")
  1094. )) || opt.replaceOptions === true
  1095. ) {
  1096. self.links[id].options = self.getElemOptions(
  1097. self.options.map.defaultLink,
  1098. (self.options.links[id] ? self.options.links[id] : {}),
  1099. {}
  1100. );
  1101. self.updateElem(self.links[id], animDuration);
  1102. }
  1103. });
  1104. // Update legends
  1105. if (opt.mapOptions && (
  1106. (typeof opt.mapOptions.legend === "object") ||
  1107. (typeof opt.mapOptions.map === "object" && typeof opt.mapOptions.map.defaultArea === "object") ||
  1108. (typeof opt.mapOptions.map === "object" && typeof opt.mapOptions.map.defaultPlot === "object")
  1109. )) {
  1110. // Show all elements on the map before updating the legends
  1111. $("[data-type='legend-elem']", self.$container).each(function (id, elem) {
  1112. if ($(elem).attr('data-hidden') === "1") {
  1113. $(elem).trigger("click", {hideOtherElems: false, animDuration: animDuration});
  1114. }
  1115. });
  1116. self.createLegends("area", self.areas, 1);
  1117. if (self.options.map.width) {
  1118. self.createLegends("plot", self.plots, (self.options.map.width / self.mapConf.width));
  1119. } else {
  1120. self.createLegends("plot", self.plots, (self.$map.width() / self.mapConf.width));
  1121. }
  1122. }
  1123. // Hide/Show all elements based on showlegendElems
  1124. // Toggle (i.e. click) only if:
  1125. // - slice legend is shown AND we want to hide
  1126. // - slice legend is hidden AND we want to show
  1127. if (typeof opt.setLegendElemsState === "object") {
  1128. // setLegendElemsState is an object listing the legend we want to hide/show
  1129. $.each(opt.setLegendElemsState, function (legendCSSClass, action) {
  1130. // Search for the legend
  1131. var $legend = self.$container.find("." + legendCSSClass)[0];
  1132. if ($legend !== undefined) {
  1133. // Select all elem inside this legend
  1134. $("[data-type='legend-elem']", $legend).each(function (id, elem) {
  1135. if (($(elem).attr('data-hidden') === "0" && action === "hide") ||
  1136. ($(elem).attr('data-hidden') === "1" && action === "show")) {
  1137. // Toggle state of element by clicking
  1138. $(elem).trigger("click", {hideOtherElems: false, animDuration: animDuration});
  1139. }
  1140. });
  1141. }
  1142. });
  1143. } else {
  1144. // setLegendElemsState is a string, or is undefined
  1145. // Default : "show"
  1146. var action = (opt.setLegendElemsState === "hide") ? "hide" : "show";
  1147. $("[data-type='legend-elem']", self.$container).each(function (id, elem) {
  1148. if (($(elem).attr('data-hidden') === "0" && action === "hide") ||
  1149. ($(elem).attr('data-hidden') === "1" && action === "show")) {
  1150. // Toggle state of element by clicking
  1151. $(elem).trigger("click", {hideOtherElems: false, animDuration: animDuration});
  1152. }
  1153. });
  1154. }
  1155. // Always rebind custom events on update
  1156. self.initDelegatedCustomEvents();
  1157. if (opt.afterUpdate) opt.afterUpdate(self.$container, self.paper, self.areas, self.plots, self.options, self.links);
  1158. },
  1159. /*
  1160. * Set plot coordinates
  1161. * @param plot object plot element
  1162. */
  1163. setPlotCoords: function(plot) {
  1164. var self = this;
  1165. if (plot.options.x !== undefined && plot.options.y !== undefined) {
  1166. plot.coords = {
  1167. x: plot.options.x,
  1168. y: plot.options.y
  1169. };
  1170. } else if (plot.options.plotsOn !== undefined && self.areas[plot.options.plotsOn] !== undefined) {
  1171. var areaBBox = self.areas[plot.options.plotsOn].mapElem.getBBox();
  1172. plot.coords = {
  1173. x: areaBBox.cx,
  1174. y: areaBBox.cy
  1175. };
  1176. } else {
  1177. plot.coords = self.mapConf.getCoords(plot.options.latitude, plot.options.longitude);
  1178. }
  1179. },
  1180. /*
  1181. * Set plot size attributes according to its type
  1182. * Note: for SVG, plot.mapElem needs to exists beforehand
  1183. * @param plot object plot element
  1184. */
  1185. setPlotAttributes: function(plot) {
  1186. if (plot.options.type === "square") {
  1187. plot.options.attrs.width = plot.options.size;
  1188. plot.options.attrs.height = plot.options.size;
  1189. plot.options.attrs.x = plot.coords.x - (plot.options.size / 2);
  1190. plot.options.attrs.y = plot.coords.y - (plot.options.size / 2);
  1191. } else if (plot.options.type === "image") {
  1192. plot.options.attrs.src = plot.options.url;
  1193. plot.options.attrs.width = plot.options.width;
  1194. plot.options.attrs.height = plot.options.height;
  1195. plot.options.attrs.x = plot.coords.x - (plot.options.width / 2);
  1196. plot.options.attrs.y = plot.coords.y - (plot.options.height / 2);
  1197. } else if (plot.options.type === "svg") {
  1198. plot.options.attrs.path = plot.options.path;
  1199. // Init transform string
  1200. if (plot.options.attrs.transform === undefined) {
  1201. plot.options.attrs.transform = "";
  1202. }
  1203. // Retrieve original boundary box if not defined
  1204. if (plot.mapElem.originalBBox === undefined) {
  1205. plot.mapElem.originalBBox = plot.mapElem.getBBox();
  1206. }
  1207. // The base transform will resize the SVG path to the one specified by width/height
  1208. // and also move the path to the actual coordinates
  1209. plot.mapElem.baseTransform = "m" + (plot.options.width / plot.mapElem.originalBBox.width) + ",0,0," +
  1210. (plot.options.height / plot.mapElem.originalBBox.height) + "," +
  1211. (plot.coords.x - plot.options.width / 2) + "," +
  1212. (plot.coords.y - plot.options.height / 2);
  1213. plot.options.attrs.transform = plot.mapElem.baseTransform + plot.options.attrs.transform;
  1214. } else { // Default : circle
  1215. plot.options.attrs.x = plot.coords.x;
  1216. plot.options.attrs.y = plot.coords.y;
  1217. plot.options.attrs.r = plot.options.size / 2;
  1218. }
  1219. },
  1220. /*
  1221. * Draw all links between plots on the paper
  1222. */
  1223. drawLinksCollection: function (linksCollection) {
  1224. var self = this;
  1225. var p1 = {};
  1226. var p2 = {};
  1227. var coordsP1 = {};
  1228. var coordsP2 = {};
  1229. var links = {};
  1230. $.each(linksCollection, function (id) {
  1231. var elemOptions = self.getElemOptions(self.options.map.defaultLink, linksCollection[id], {});
  1232. if (typeof linksCollection[id].between[0] === 'string') {
  1233. p1 = self.options.plots[linksCollection[id].between[0]];
  1234. } else {
  1235. p1 = linksCollection[id].between[0];
  1236. }
  1237. if (typeof linksCollection[id].between[1] === 'string') {
  1238. p2 = self.options.plots[linksCollection[id].between[1]];
  1239. } else {
  1240. p2 = linksCollection[id].between[1];
  1241. }
  1242. if (p1.plotsOn !== undefined && self.areas[p1.plotsOn] !== undefined) {
  1243. var p1BBox = self.areas[p1.plotsOn].mapElem.getBBox();
  1244. coordsP1 = {
  1245. x: p1BBox.cx,
  1246. y: p1BBox.cy
  1247. };
  1248. }
  1249. else if (p1.latitude !== undefined && p1.longitude !== undefined) {
  1250. coordsP1 = self.mapConf.getCoords(p1.latitude, p1.longitude);
  1251. } else {
  1252. coordsP1.x = p1.x;
  1253. coordsP1.y = p1.y;
  1254. }
  1255. if (p2.plotsOn !== undefined && self.areas[p2.plotsOn] !== undefined) {
  1256. var p2BBox = self.areas[p2.plotsOn].mapElem.getBBox();
  1257. coordsP2 = {
  1258. x: p2BBox.cx,
  1259. y: p2BBox.cy
  1260. };
  1261. }
  1262. else if (p2.latitude !== undefined && p2.longitude !== undefined) {
  1263. coordsP2 = self.mapConf.getCoords(p2.latitude, p2.longitude);
  1264. } else {
  1265. coordsP2.x = p2.x;
  1266. coordsP2.y = p2.y;
  1267. }
  1268. links[id] = self.drawLink(id, coordsP1.x, coordsP1.y, coordsP2.x, coordsP2.y, elemOptions);
  1269. });
  1270. return links;
  1271. },
  1272. /*
  1273. * Draw a curved link between two couples of coordinates a(xa,ya) and b(xb, yb) on the paper
  1274. */
  1275. drawLink: function (id, xa, ya, xb, yb, elemOptions) {
  1276. var self = this;
  1277. var link = {
  1278. options: elemOptions
  1279. };
  1280. // Compute the "curveto" SVG point, d(x,y)
  1281. // c(xc, yc) is the center of (xa,ya) and (xb, yb)
  1282. var xc = (xa + xb) / 2;
  1283. var yc = (ya + yb) / 2;
  1284. // Equation for (cd) : y = acd * x + bcd (d is the cure point)
  1285. var acd = -1 / ((yb - ya) / (xb - xa));
  1286. var bcd = yc - acd * xc;
  1287. // dist(c,d) = dist(a,b) (=abDist)
  1288. var abDist = Math.sqrt((xb - xa) * (xb - xa) + (yb - ya) * (yb - ya));
  1289. // Solution for equation dist(cd) = sqrt((xd - xc)² + (yd - yc)²)
  1290. // dist(c,d)² = (xd - xc)² + (yd - yc)²
  1291. // We assume that dist(c,d) = dist(a,b)
  1292. // so : (xd - xc)² + (yd - yc)² - dist(a,b)² = 0
  1293. // With the factor : (xd - xc)² + (yd - yc)² - (factor*dist(a,b))² = 0
  1294. // (xd - xc)² + (acd*xd + bcd - yc)² - (factor*dist(a,b))² = 0
  1295. var a = 1 + acd * acd;
  1296. var b = -2 * xc + 2 * acd * bcd - 2 * acd * yc;
  1297. var c = xc * xc + bcd * bcd - bcd * yc - yc * bcd + yc * yc - ((elemOptions.factor * abDist) * (elemOptions.factor * abDist));
  1298. var delta = b * b - 4 * a * c;
  1299. var x = 0;
  1300. var y = 0;
  1301. // There are two solutions, we choose one or the other depending on the sign of the factor
  1302. if (elemOptions.factor > 0) {
  1303. x = (-b + Math.sqrt(delta)) / (2 * a);
  1304. y = acd * x + bcd;
  1305. } else {
  1306. x = (-b - Math.sqrt(delta)) / (2 * a);
  1307. y = acd * x + bcd;
  1308. }
  1309. link.mapElem = self.paper.path("m " + xa + "," + ya + " C " + x + "," + y + " " + xb + "," + yb + " " + xb + "," + yb + "");
  1310. self.initElem(id, 'link', link);
  1311. return link;
  1312. },
  1313. /*
  1314. * Check wether newAttrs object bring modifications to originalAttrs object
  1315. */
  1316. isAttrsChanged: function(originalAttrs, newAttrs) {
  1317. for (var key in newAttrs) {
  1318. if (newAttrs.hasOwnProperty(key) && typeof originalAttrs[key] === 'undefined' || newAttrs[key] !== originalAttrs[key]) {
  1319. return true;
  1320. }
  1321. }
  1322. return false;
  1323. },
  1324. /*
  1325. * Update the element "elem" on the map with the new options
  1326. */
  1327. updateElem: function (elem, animDuration) {
  1328. var self = this;
  1329. var mapElemBBox;
  1330. var plotOffsetX;
  1331. var plotOffsetY;
  1332. if (elem.options.toFront === true) {
  1333. elem.mapElem.toFront();
  1334. }
  1335. // Set the cursor attribute related to the HTML link
  1336. if (elem.options.href !== undefined) {
  1337. elem.options.attrs.cursor = "pointer";
  1338. if (elem.options.text) elem.options.text.attrs.cursor = "pointer";
  1339. } else {
  1340. // No HTML links, check if a cursor was defined to pointer
  1341. if (elem.mapElem.attrs.cursor === 'pointer') {
  1342. elem.options.attrs.cursor = "auto";
  1343. if (elem.options.text) elem.options.text.attrs.cursor = "auto";
  1344. }
  1345. }
  1346. // Update the label
  1347. if (elem.textElem) {
  1348. // Update text attr
  1349. elem.options.text.attrs.text = elem.options.text.content;
  1350. // Get mapElem size, and apply an offset to handle future width/height change
  1351. mapElemBBox = elem.mapElem.getBBox();
  1352. if (elem.options.size || (elem.options.width && elem.options.height)) {
  1353. if (elem.options.type === "image" || elem.options.type === "svg") {
  1354. plotOffsetX = (elem.options.width - mapElemBBox.width) / 2;
  1355. plotOffsetY = (elem.options.height - mapElemBBox.height) / 2;
  1356. } else {
  1357. plotOffsetX = (elem.options.size - mapElemBBox.width) / 2;
  1358. plotOffsetY = (elem.options.size - mapElemBBox.height) / 2;
  1359. }
  1360. mapElemBBox.x -= plotOffsetX;
  1361. mapElemBBox.x2 += plotOffsetX;
  1362. mapElemBBox.y -= plotOffsetY;
  1363. mapElemBBox.y2 += plotOffsetY;
  1364. }
  1365. // Update position attr
  1366. var textPosition = self.getTextPosition(mapElemBBox, elem.options.text.position, elem.options.text.margin);
  1367. elem.options.text.attrs.x = textPosition.x;
  1368. elem.options.text.attrs.y = textPosition.y;
  1369. elem.options.text.attrs['text-anchor'] = textPosition.textAnchor;
  1370. // Update text element attrs and attrsHover
  1371. self.setHoverOptions(elem.textElem, elem.options.text.attrs, elem.options.text.attrsHover);
  1372. if (self.isAttrsChanged(elem.textElem.attrs, elem.options.text.attrs)) {
  1373. self.animate(elem.textElem, elem.options.text.attrs, animDuration);
  1374. }
  1375. }
  1376. // Update elements attrs and attrsHover
  1377. self.setHoverOptions(elem.mapElem, elem.options.attrs, elem.options.attrsHover);
  1378. if (self.isAttrsChanged(elem.mapElem.attrs, elem.options.attrs)) {
  1379. self.animate(elem.mapElem, elem.options.attrs, animDuration);
  1380. }
  1381. // Update the cssClass
  1382. if (elem.options.cssClass !== undefined) {
  1383. $(elem.mapElem.node).removeClass().addClass(elem.options.cssClass);
  1384. }
  1385. },
  1386. /*
  1387. * Draw the plot
  1388. */
  1389. drawPlot: function (id) {
  1390. var self = this;
  1391. var plot = {};
  1392. // Get plot options and store it
  1393. plot.options = self.getElemOptions(
  1394. self.options.map.defaultPlot,
  1395. (self.options.plots[id] ? self.options.plots[id] : {}),
  1396. self.options.legend.plot
  1397. );
  1398. // Set plot coords
  1399. self.setPlotCoords(plot);
  1400. // Draw SVG before setPlotAttributes()
  1401. if (plot.options.type === "svg") {
  1402. plot.mapElem = self.paper.path(plot.options.path);
  1403. }
  1404. // Set plot size attrs
  1405. self.setPlotAttributes(plot);
  1406. // Draw other types of plots
  1407. if (plot.options.type === "square") {
  1408. plot.mapElem = self.paper.rect(
  1409. plot.options.attrs.x,
  1410. plot.options.attrs.y,
  1411. plot.options.attrs.width,
  1412. plot.options.attrs.height
  1413. );
  1414. } else if (plot.options.type === "image") {
  1415. plot.mapElem = self.paper.image(
  1416. plot.options.attrs.src,
  1417. plot.options.attrs.x,
  1418. plot.options.attrs.y,
  1419. plot.options.attrs.width,
  1420. plot.options.attrs.height
  1421. );
  1422. } else if (plot.options.type === "svg") {
  1423. // Nothing to do
  1424. } else {
  1425. // Default = circle
  1426. plot.mapElem = self.paper.circle(
  1427. plot.options.attrs.x,
  1428. plot.options.attrs.y,
  1429. plot.options.attrs.r
  1430. );
  1431. }
  1432. self.initElem(id, 'plot', plot);
  1433. return plot;
  1434. },
  1435. /*
  1436. * Set user defined handlers for events on areas and plots
  1437. * @param id the id of the element
  1438. * @param type the type of the element (area, plot, link)
  1439. * @param elem the element object {mapElem, textElem, options, ...}
  1440. */
  1441. setEventHandlers: function (id, type, elem) {
  1442. var self = this;
  1443. $.each(elem.options.eventHandlers, function (event) {
  1444. if (self.customEventHandlers[event] === undefined) self.customEventHandlers[event] = {};
  1445. if (self.customEventHandlers[event][type] === undefined) self.customEventHandlers[event][type] = {};
  1446. self.customEventHandlers[event][type][id] = elem;
  1447. });
  1448. },
  1449. /*
  1450. * Draw a legend for areas and / or plots
  1451. * @param legendOptions options for the legend to draw
  1452. * @param legendType the type of the legend : "area" or "plot"
  1453. * @param elems collection of plots or areas on the maps
  1454. * @param legendIndex index of the legend in the conf array
  1455. */
  1456. drawLegend: function (legendOptions, legendType, elems, scale, legendIndex) {
  1457. var self = this;
  1458. var $legend = {};
  1459. var legendPaper = {};
  1460. var width = 0;
  1461. var height = 0;
  1462. var title = null;
  1463. var titleBBox = null;
  1464. var legendElems = {};
  1465. var i = 0;
  1466. var x = 0;
  1467. var y = 0;
  1468. var yCenter = 0;
  1469. var sliceOptions = [];
  1470. $legend = $("." + legendOptions.cssClass, self.$container);
  1471. // Save content for later
  1472. var initialHTMLContent = $legend.html();
  1473. $legend.empty();
  1474. legendPaper = new Raphael($legend.get(0));
  1475. // Set some data to object
  1476. $(legendPaper.canvas).attr({"data-legend-type": legendType, "data-legend-id": legendIndex});
  1477. height = width = 0;
  1478. // Set the title of the legend
  1479. if (legendOptions.title && legendOptions.title !== "") {
  1480. title = legendPaper.text(legendOptions.marginLeftTitle, 0, legendOptions.title).attr(legendOptions.titleAttrs);
  1481. titleBBox = title.getBBox();
  1482. title.attr({y: 0.5 * titleBBox.height});
  1483. width = legendOptions.marginLeftTitle + titleBBox.width;
  1484. height += legendOptions.marginBottomTitle + titleBBox.height;
  1485. }
  1486. // Calculate attrs (and width, height and r (radius)) for legend elements, and yCenter for horizontal legends
  1487. for (i = 0; i < legendOptions.slices.length; ++i) {
  1488. var yCenterCurrent = 0;
  1489. sliceOptions[i] = $.extend(true, {}, (legendType === "plot") ? self.options.map.defaultPlot : self.options.map.defaultArea, legendOptions.slices[i]);
  1490. if (legendOptions.slices[i].legendSpecificAttrs === undefined) {
  1491. legendOptions.slices[i].legendSpecificAttrs = {};
  1492. }
  1493. $.extend(true, sliceOptions[i].attrs, legendOptions.slices[i].legendSpecificAttrs);
  1494. if (legendType === "area") {
  1495. if (sliceOptions[i].attrs.width === undefined)
  1496. sliceOptions[i].attrs.width = 30;
  1497. if (sliceOptions[i].attrs.height === undefined)
  1498. sliceOptions[i].attrs.height = 20;
  1499. } else if (sliceOptions[i].type === "square") {
  1500. if (sliceOptions[i].attrs.width === undefined)
  1501. sliceOptions[i].attrs.width = sliceOptions[i].size;
  1502. if (sliceOptions[i].attrs.height === undefined)
  1503. sliceOptions[i].attrs.height = sliceOptions[i].size;
  1504. } else if (sliceOptions[i].type === "image" || sliceOptions[i].type === "svg") {
  1505. if (sliceOptions[i].attrs.width === undefined)
  1506. sliceOptions[i].attrs.width = sliceOptions[i].width;
  1507. if (sliceOptions[i].attrs.height === undefined)
  1508. sliceOptions[i].attrs.height = sliceOptions[i].height;
  1509. } else {
  1510. if (sliceOptions[i].attrs.r === undefined)
  1511. sliceOptions[i].attrs.r = sliceOptions[i].size / 2;
  1512. }
  1513. // Compute yCenter for this legend slice
  1514. yCenterCurrent = legendOptions.marginBottomTitle;
  1515. // Add title height if it exists
  1516. if (title) {
  1517. yCenterCurrent += titleBBox.height;
  1518. }
  1519. if (legendType === "plot" && (sliceOptions[i].type === undefined || sliceOptions[i].type === "circle")) {
  1520. yCenterCurrent += scale * sliceOptions[i].attrs.r;
  1521. } else {
  1522. yCenterCurrent += scale * sliceOptions[i].attrs.height / 2;
  1523. }
  1524. // Update yCenter if current larger
  1525. yCenter = Math.max(yCenter, yCenterCurrent);
  1526. }
  1527. if (legendOptions.mode === "horizontal") {
  1528. width = legendOptions.marginLeft;
  1529. }
  1530. // Draw legend elements (circle, square or image in vertical or horizontal mode)
  1531. for (i = 0; i < sliceOptions.length; ++i) {
  1532. var legendElem = {};
  1533. var legendElemBBox = {};
  1534. var legendLabel = {};
  1535. if (sliceOptions[i].display === undefined || sliceOptions[i].display === true) {
  1536. if (legendType === "area") {
  1537. if (legendOptions.mode === "horizontal") {
  1538. x = width + legendOptions.marginLeft;
  1539. y = yCenter - (0.5 * scale * sliceOptions[i].attrs.height);
  1540. } else {
  1541. x = legendOptions.marginLeft;
  1542. y = height;
  1543. }
  1544. legendElem = legendPaper.rect(x, y, scale * (sliceOptions[i].attrs.width), scale * (sliceOptions[i].attrs.height));
  1545. } else if (sliceOptions[i].type === "square") {
  1546. if (legendOptions.mode === "horizontal") {
  1547. x = width + legendOptions.marginLeft;
  1548. y = yCenter - (0.5 * scale * sliceOptions[i].attrs.height);
  1549. } else {
  1550. x = legendOptions.marginLeft;
  1551. y = height;
  1552. }
  1553. legendElem = legendPaper.rect(x, y, scale * (sliceOptions[i].attrs.width), scale * (sliceOptions[i].attrs.height));
  1554. } else if (sliceOptions[i].type === "image" || sliceOptions[i].type === "svg") {
  1555. if (legendOptions.mode === "horizontal") {
  1556. x = width + legendOptions.marginLeft;
  1557. y = yCenter - (0.5 * scale * sliceOptions[i].attrs.height);
  1558. } else {
  1559. x = legendOptions.marginLeft;
  1560. y = height;
  1561. }
  1562. if (sliceOptions[i].type === "image") {
  1563. legendElem = legendPaper.image(
  1564. sliceOptions[i].url, x, y, scale * sliceOptions[i].attrs.width, scale * sliceOptions[i].attrs.height);
  1565. } else {
  1566. legendElem = legendPaper.path(sliceOptions[i].path);
  1567. if (sliceOptions[i].attrs.transform === undefined) {
  1568. sliceOptions[i].attrs.transform = "";
  1569. }
  1570. legendElemBBox = legendElem.getBBox();
  1571. sliceOptions[i].attrs.transform = "m" + ((scale * sliceOptions[i].width) / legendElemBBox.width) + ",0,0," + ((scale * sliceOptions[i].height) / legendElemBBox.height) + "," + x + "," + y + sliceOptions[i].attrs.transform;
  1572. }
  1573. } else {
  1574. if (legendOptions.mode === "horizontal") {
  1575. x = width + legendOptions.marginLeft + scale * (sliceOptions[i].attrs.r);
  1576. y = yCenter;
  1577. } else {
  1578. x = legendOptions.marginLeft + scale * (sliceOptions[i].attrs.r);
  1579. y = height + scale * (sliceOptions[i].attrs.r);
  1580. }
  1581. legendElem = legendPaper.circle(x, y, scale * (sliceOptions[i].attrs.r));
  1582. }
  1583. // Set attrs to the element drawn above
  1584. delete sliceOptions[i].attrs.width;
  1585. delete sliceOptions[i].attrs.height;
  1586. delete sliceOptions[i].attrs.r;
  1587. legendElem.attr(sliceOptions[i].attrs);
  1588. legendElemBBox = legendElem.getBBox();
  1589. // Draw the label associated with the element
  1590. if (legendOptions.mode === "horizontal") {
  1591. x = width + legendOptions.marginLeft + legendElemBBox.width + legendOptions.marginLeftLabel;
  1592. y = yCenter;
  1593. } else {
  1594. x = legendOptions.marginLeft + legendElemBBox.width + legendOptions.marginLeftLabel;
  1595. y = height + (legendElemBBox.height / 2);
  1596. }
  1597. legendLabel = legendPaper.text(x, y, sliceOptions[i].label).attr(legendOptions.labelAttrs);
  1598. // Update the width and height for the paper
  1599. if (legendOptions.mode === "horizontal") {
  1600. var currentHeight = legendOptions.marginBottom + legendElemBBox.height;
  1601. width += legendOptions.marginLeft + legendElemBBox.width + legendOptions.marginLeftLabel + legendLabel.getBBox().width;
  1602. if (sliceOptions[i].type !== "image" && legendType !== "area") {
  1603. currentHeight += legendOptions.marginBottomTitle;
  1604. }
  1605. // Add title height if it exists
  1606. if (title) {
  1607. currentHeight += titleBBox.height;
  1608. }
  1609. height = Math.max(height, currentHeight);
  1610. } else {
  1611. width = Math.max(width, legendOptions.marginLeft + legendElemBBox.width + legendOptions.marginLeftLabel + legendLabel.getBBox().width);
  1612. height += legendOptions.marginBottom + legendElemBBox.height;
  1613. }
  1614. // Set some data to elements
  1615. $(legendElem.node).attr({
  1616. "data-legend-id": legendIndex,
  1617. "data-legend-type": legendType,
  1618. "data-type": "legend-elem",
  1619. "data-id": i,
  1620. "data-hidden": 0
  1621. });
  1622. $(legendLabel.node).attr({
  1623. "data-legend-id": legendIndex,
  1624. "data-legend-type": legendType,
  1625. "data-type": "legend-label",
  1626. "data-id": i,
  1627. "data-hidden": 0
  1628. });
  1629. // Set array content
  1630. // We use similar names like map/plots/links
  1631. legendElems[i] = {
  1632. mapElem: legendElem,
  1633. textElem: legendLabel
  1634. };
  1635. // Hide map elements when the user clicks on a legend item
  1636. if (legendOptions.hideElemsOnClick.enabled) {
  1637. // Hide/show elements when user clicks on a legend element
  1638. legendLabel.attr({cursor: "pointer"});
  1639. legendElem.attr({cursor: "pointer"});
  1640. self.setHoverOptions(legendElem, sliceOptions[i].attrs, sliceOptions[i].attrs);
  1641. self.setHoverOptions(legendLabel, legendOptions.labelAttrs, legendOptions.labelAttrsHover);
  1642. if (sliceOptions[i].clicked !== undefined && sliceOptions[i].clicked === true) {
  1643. self.handleClickOnLegendElem(legendElems[i], i, legendIndex, legendType, {hideOtherElems: false});
  1644. }
  1645. }
  1646. }
  1647. }
  1648. // VMLWidth option allows you to set static width for the legend
  1649. // only for VML render because text.getBBox() returns wrong values on IE6/7
  1650. if (Raphael.type !== "SVG" && legendOptions.VMLWidth)
  1651. width = legendOptions.VMLWidth;
  1652. legendPaper.setSize(width, height);
  1653. return {
  1654. container: $legend,
  1655. initialHTMLContent: initialHTMLContent,
  1656. elems: legendElems
  1657. };
  1658. },
  1659. /*
  1660. * Allow to hide elements of the map when the user clicks on a related legend item
  1661. * @param elem legend element
  1662. * @param id legend element ID
  1663. * @param legendIndex corresponding legend index
  1664. * @param legendType corresponding legend type (area or plot)
  1665. * @param opts object additionnal options
  1666. * hideOtherElems boolean, if other elems shall be hidden
  1667. * animDuration duration of animation
  1668. */
  1669. handleClickOnLegendElem: function(elem, id, legendIndex, legendType, opts) {
  1670. var self = this;
  1671. var legendOptions;
  1672. opts = opts || {};
  1673. if (!$.isArray(self.options.legend[legendType])) {
  1674. legendOptions = self.options.legend[legendType];
  1675. } else {
  1676. legendOptions = self.options.legend[legendType][legendIndex];
  1677. }
  1678. var legendElem = elem.mapElem;
  1679. var legendLabel = elem.textElem;
  1680. var $legendElem = $(legendElem.node);
  1681. var $legendLabel = $(legendLabel.node);
  1682. var sliceOptions = legendOptions.slices[id];
  1683. var mapElems = legendType === 'area' ? self.areas : self.plots;
  1684. // Check animDuration: if not set, this is a regular click, use the value specified in options
  1685. var animDuration = opts.animDuration !== undefined ? opts.animDuration : legendOptions.hideElemsOnClick.animDuration ;
  1686. var hidden = $legendElem.attr('data-hidden');
  1687. var hiddenNewAttr = (hidden === '0') ? {"data-hidden": '1'} : {"data-hidden": '0'};
  1688. if (hidden === '0') {
  1689. self.animate(legendLabel, {"opacity": 0.5}, animDuration);
  1690. } else {
  1691. self.animate(legendLabel, {"opacity": 1}, animDuration);
  1692. }
  1693. $.each(mapElems, function (y) {
  1694. var elemValue;
  1695. // Retreive stored data of element
  1696. // 'hidden-by' contains the list of legendIndex that is hiding this element
  1697. var hiddenBy = mapElems[y].mapElem.data('hidden-by');
  1698. // Set to empty object if undefined
  1699. if (hiddenBy === undefined) hiddenBy = {};
  1700. if ($.isArray(mapElems[y].options.value)) {
  1701. elemValue = mapElems[y].options.value[legendIndex];
  1702. } else {
  1703. elemValue = mapElems[y].options.value;
  1704. }
  1705. // Hide elements whose value matches with the slice of the clicked legend item
  1706. if (self.getLegendSlice(elemValue, legendOptions) === sliceOptions) {
  1707. if (hidden === '0') { // we want to hide this element
  1708. hiddenBy[legendIndex] = true; // add legendIndex to the data object for later use
  1709. self.setElementOpacity(mapElems[y], legendOptions.hideElemsOnClick.opacity, animDuration);
  1710. } else { // We want to show this element
  1711. delete hiddenBy[legendIndex]; // Remove this legendIndex from object
  1712. // Check if another legendIndex is defined
  1713. // We will show this element only if no legend is no longer hiding it
  1714. if ($.isEmptyObject(hiddenBy)) {
  1715. self.setElementOpacity(
  1716. mapElems[y],
  1717. mapElems[y].mapElem.originalAttrs.opacity !== undefined ? mapElems[y].mapElem.originalAttrs.opacity : 1,
  1718. animDuration
  1719. );
  1720. }
  1721. }
  1722. // Update elem data with new values
  1723. mapElems[y].mapElem.data('hidden-by', hiddenBy);
  1724. }
  1725. });
  1726. $legendElem.attr(hiddenNewAttr);
  1727. $legendLabel.attr(hiddenNewAttr);
  1728. if ((opts.hideOtherElems === undefined || opts.hideOtherElems === true) && legendOptions.exclusive === true ) {
  1729. $("[data-type='legend-elem'][data-hidden=0]", self.$container).each(function () {
  1730. var $elem = $(this);
  1731. if ($elem.attr('data-id') !== id) {
  1732. $elem.trigger("click", {hideOtherElems: false});
  1733. }
  1734. });
  1735. }
  1736. },
  1737. /*
  1738. * Create all legends for a specified type (area or plot)
  1739. * @param legendType the type of the legend : "area" or "plot"
  1740. * @param elems collection of plots or areas displayed on the map
  1741. * @param scale scale ratio of the map
  1742. */
  1743. createLegends: function (legendType, elems, scale) {
  1744. var self = this;
  1745. var legendsOptions = self.options.legend[legendType];
  1746. if (!$.isArray(self.options.legend[legendType])) {
  1747. legendsOptions = [self.options.legend[legendType]];
  1748. }
  1749. self.legends[legendType] = {};
  1750. for (var j = 0; j < legendsOptions.length; ++j) {
  1751. if (legendsOptions[j].display === true && $.isArray(legendsOptions[j].slices) && legendsOptions[j].slices.length > 0 &&
  1752. legendsOptions[j].cssClass !== "" && $("." + legendsOptions[j].cssClass, self.$container).length !== 0
  1753. ) {
  1754. self.legends[legendType][j] = self.drawLegend(legendsOptions[j], legendType, elems, scale, j);
  1755. }
  1756. }
  1757. },
  1758. /*
  1759. * Set the attributes on hover and the attributes to restore for a map element
  1760. * @param elem the map element
  1761. * @param originalAttrs the original attributes to restore on mouseout event
  1762. * @param attrsHover the attributes to set on mouseover event
  1763. */
  1764. setHoverOptions: function (elem, originalAttrs, attrsHover) {
  1765. // Disable transform option on hover for VML (IE<9) because of several bugs
  1766. if (Raphael.type !== "SVG") delete attrsHover.transform;
  1767. elem.attrsHover = attrsHover;
  1768. if (elem.attrsHover.transform) elem.originalAttrs = $.extend({transform: "s1"}, originalAttrs);
  1769. else elem.originalAttrs = originalAttrs;
  1770. },
  1771. /*
  1772. * Set the behaviour when mouse enters element ("mouseover" event)
  1773. * It may be an area, a plot, a link or a legend element
  1774. * @param elem the map element
  1775. */
  1776. elemEnter: function (elem) {
  1777. var self = this;
  1778. if (elem === undefined) return;
  1779. /* Handle mapElem Hover attributes */
  1780. if (elem.mapElem !== undefined) {
  1781. self.animate(elem.mapElem, elem.mapElem.attrsHover, elem.mapElem.attrsHover.animDuration);
  1782. }
  1783. /* Handle textElem Hover attributes */
  1784. if (elem.textElem !== undefined) {
  1785. self.animate(elem.textElem, elem.textElem.attrsHover, elem.textElem.attrsHover.animDuration);
  1786. }
  1787. /* Handle tooltip init */
  1788. if (elem.options && elem.options.tooltip !== undefined) {
  1789. var content = '';
  1790. // Reset classes
  1791. self.$tooltip.removeClass().addClass(self.options.map.tooltip.cssClass);
  1792. // Get content
  1793. if (elem.options.tooltip.content !== undefined) {
  1794. // if tooltip.content is function, call it. Otherwise, assign it directly.
  1795. if (typeof elem.options.tooltip.content === "function") content = elem.options.tooltip.content(elem.mapElem);
  1796. else content = elem.options.tooltip.content;
  1797. }
  1798. if (elem.options.tooltip.cssClass !== undefined) {
  1799. self.$tooltip.addClass(elem.options.tooltip.cssClass);
  1800. }
  1801. self.$tooltip.html(content).css("display", "block");
  1802. }
  1803. // workaround for older version of Raphael
  1804. if (elem.mapElem !== undefined || elem.textElem !== undefined) {
  1805. if (self.paper.safari) self.paper.safari();
  1806. }
  1807. },
  1808. /*
  1809. * Set the behaviour when mouse moves in element ("mousemove" event)
  1810. * @param elem the map element
  1811. */
  1812. elemHover: function (elem, event) {
  1813. var self = this;
  1814. if (elem === undefined) return;
  1815. /* Handle tooltip position update */
  1816. if (elem.options.tooltip !== undefined) {
  1817. var mouseX = event.pageX;
  1818. var mouseY = event.pageY;
  1819. var offsetLeft = 10;
  1820. var offsetTop = 20;
  1821. if (typeof elem.options.tooltip.offset === "object") {
  1822. if (typeof elem.options.tooltip.offset.left !== "undefined") {
  1823. offsetLeft = elem.options.tooltip.offset.left;
  1824. }
  1825. if (typeof elem.options.tooltip.offset.top !== "undefined") {
  1826. offsetTop = elem.options.tooltip.offset.top;
  1827. }
  1828. }
  1829. var tooltipPosition = {
  1830. "left": Math.min(self.$map.width() - self.$tooltip.outerWidth() - 5,
  1831. mouseX - self.$map.offset().left + offsetLeft),
  1832. "top": Math.min(self.$map.height() - self.$tooltip.outerHeight() - 5,
  1833. mouseY - self.$map.offset().top + offsetTop)
  1834. };
  1835. if (typeof elem.options.tooltip.overflow === "object") {
  1836. if (elem.options.tooltip.overflow.right === true) {
  1837. tooltipPosition.left = mouseX - self.$map.offset().left + 10;
  1838. }
  1839. if (elem.options.tooltip.overflow.bottom === true) {
  1840. tooltipPosition.top = mouseY - self.$map.offset().top + 20;
  1841. }
  1842. }
  1843. self.$tooltip.css(tooltipPosition);
  1844. }
  1845. },
  1846. /*
  1847. * Set the behaviour when mouse leaves element ("mouseout" event)
  1848. * It may be an area, a plot, a link or a legend element
  1849. * @param elem the map element
  1850. */
  1851. elemOut: function (elem) {
  1852. var self = this;
  1853. if (elem === undefined) return;
  1854. /* reset mapElem attributes */
  1855. if (elem.mapElem !== undefined) {
  1856. self.animate(elem.mapElem, elem.mapElem.originalAttrs, elem.mapElem.attrsHover.animDuration);
  1857. }
  1858. /* reset textElem attributes */
  1859. if (elem.textElem !== undefined) {
  1860. self.animate(elem.textElem, elem.textElem.originalAttrs, elem.textElem.attrsHover.animDuration);
  1861. }
  1862. /* reset tooltip */
  1863. if (elem.options && elem.options.tooltip !== undefined) {
  1864. self.$tooltip.css({
  1865. 'display': 'none',
  1866. 'top': -1000,
  1867. 'left': -1000
  1868. });
  1869. }
  1870. // workaround for older version of Raphael
  1871. if (elem.mapElem !== undefined || elem.textElem !== undefined) {
  1872. if (self.paper.safari) self.paper.safari();
  1873. }
  1874. },
  1875. /*
  1876. * Set the behaviour when mouse clicks element ("click" event)
  1877. * It may be an area, a plot or a link (but not a legend element which has its own function)
  1878. * @param elem the map element
  1879. */
  1880. elemClick: function (elem) {
  1881. var self = this;
  1882. if (elem === undefined) return;
  1883. /* Handle click when href defined */
  1884. if (!self.panning && elem.options.href !== undefined) {
  1885. window.open(elem.options.href, elem.options.target);
  1886. }
  1887. },
  1888. /*
  1889. * Get element options by merging default options, element options and legend options
  1890. * @param defaultOptions
  1891. * @param elemOptions
  1892. * @param legendOptions
  1893. */
  1894. getElemOptions: function (defaultOptions, elemOptions, legendOptions) {
  1895. var self = this;
  1896. var options = $.extend(true, {}, defaultOptions, elemOptions);
  1897. if (options.value !== undefined) {
  1898. if ($.isArray(legendOptions)) {
  1899. for (var i = 0; i < legendOptions.length; ++i) {
  1900. options = $.extend(true, {}, options, self.getLegendSlice(options.value[i], legendOptions[i]));
  1901. }
  1902. } else {
  1903. options = $.extend(true, {}, options, self.getLegendSlice(options.value, legendOptions));
  1904. }
  1905. }
  1906. return options;
  1907. },
  1908. /*
  1909. * Get the coordinates of the text relative to a bbox and a position
  1910. * @param bbox the boundary box of the element
  1911. * @param textPosition the wanted text position (inner, right, left, top or bottom)
  1912. * @param margin number or object {x: val, y:val} margin between the bbox and the text
  1913. */
  1914. getTextPosition: function (bbox, textPosition, margin) {
  1915. var textX = 0;
  1916. var textY = 0;
  1917. var textAnchor = "";
  1918. if (typeof margin === "number") {
  1919. if (textPosition === "bottom" || textPosition === "top") {
  1920. margin = {x: 0, y: margin};
  1921. } else if (textPosition === "right" || textPosition === "left") {
  1922. margin = {x: margin, y: 0};
  1923. } else {
  1924. margin = {x: 0, y: 0};
  1925. }
  1926. }
  1927. switch (textPosition) {
  1928. case "bottom" :
  1929. textX = ((bbox.x + bbox.x2) / 2) + margin.x;
  1930. textY = bbox.y2 + margin.y;
  1931. textAnchor = "middle";
  1932. break;
  1933. case "top" :
  1934. textX = ((bbox.x + bbox.x2) / 2) + margin.x;
  1935. textY = bbox.y - margin.y;
  1936. textAnchor = "middle";
  1937. break;
  1938. case "left" :
  1939. textX = bbox.x - margin.x;
  1940. textY = ((bbox.y + bbox.y2) / 2) + margin.y;
  1941. textAnchor = "end";
  1942. break;
  1943. case "right" :
  1944. textX = bbox.x2 + margin.x;
  1945. textY = ((bbox.y + bbox.y2) / 2) + margin.y;
  1946. textAnchor = "start";
  1947. break;
  1948. default : // "inner" position
  1949. textX = ((bbox.x + bbox.x2) / 2) + margin.x;
  1950. textY = ((bbox.y + bbox.y2) / 2) + margin.y;
  1951. textAnchor = "middle";
  1952. }
  1953. return {"x": textX, "y": textY, "textAnchor": textAnchor};
  1954. },
  1955. /*
  1956. * Get the legend conf matching with the value
  1957. * @param value the value to match with a slice in the legend
  1958. * @param legend the legend params object
  1959. * @return the legend slice matching with the value
  1960. */
  1961. getLegendSlice: function (value, legend) {
  1962. for (var i = 0; i < legend.slices.length; ++i) {
  1963. if ((legend.slices[i].sliceValue !== undefined && value === legend.slices[i].sliceValue) ||
  1964. ((legend.slices[i].sliceValue === undefined) &&
  1965. (legend.slices[i].min === undefined || value >= legend.slices[i].min) &&
  1966. (legend.slices[i].max === undefined || value <= legend.slices[i].max))
  1967. ) {
  1968. return legend.slices[i];
  1969. }
  1970. }
  1971. return {};
  1972. },
  1973. /*
  1974. * Animated view box changes
  1975. * As from http://code.voidblossom.com/animating-viewbox-easing-formulas/,
  1976. * (from https://github.com/theshaun works on mapael)
  1977. * @param x coordinate of the point to focus on
  1978. * @param y coordinate of the point to focus on
  1979. * @param w map defined width
  1980. * @param h map defined height
  1981. * @param duration defined length of time for animation
  1982. * @param easingFunction defined Raphael supported easing_formula to use
  1983. */
  1984. animateViewBox: function (targetX, targetY, targetW, targetH, duration, easingFunction) {
  1985. var self = this;
  1986. var cx = self.currentViewBox.x;
  1987. var dx = targetX - cx;
  1988. var cy = self.currentViewBox.y;
  1989. var dy = targetY - cy;
  1990. var cw = self.currentViewBox.w;
  1991. var dw = targetW - cw;
  1992. var ch = self.currentViewBox.h;
  1993. var dh = targetH - ch;
  1994. // Init current ViewBox target if undefined
  1995. if (!self.zoomAnimCVBTarget) {
  1996. self.zoomAnimCVBTarget = {
  1997. x: targetX, y: targetY, w: targetW, h: targetH
  1998. };
  1999. }
  2000. // Determine zoom direction by comparig current vs. target width
  2001. var zoomDir = (cw > targetW) ? 'in' : 'out';
  2002. var easingFormula = Raphael.easing_formulas[easingFunction || "linear"];
  2003. // To avoid another frame when elapsed time approach end (2%)
  2004. var durationWithMargin = duration - (duration * 2 / 100);
  2005. // Save current zoomAnimStartTime before assigning a new one
  2006. var oldZoomAnimStartTime = self.zoomAnimStartTime;
  2007. self.zoomAnimStartTime = (new Date()).getTime();
  2008. /* Actual function to animate the ViewBox
  2009. * Uses requestAnimationFrame to schedule itself again until animation is over
  2010. */
  2011. var computeNextStep = function () {
  2012. // Cancel any remaining animationFrame
  2013. // It means this new step will take precedence over the old one scheduled
  2014. // This is the case when the user is triggering the zoom fast (e.g. with a big mousewheel run)
  2015. // This actually does nothing when performing a single zoom action
  2016. self.cancelAnimationFrame(self.zoomAnimID);
  2017. // Compute elapsed time
  2018. var elapsed = (new Date()).getTime() - self.zoomAnimStartTime;
  2019. // Check if animation should finish
  2020. if (elapsed < durationWithMargin) {
  2021. // Hold the future ViewBox values
  2022. var x, y, w, h;
  2023. // There are two ways to compute the next ViewBox size
  2024. // 1. If the target ViewBox has changed between steps (=> ADAPTATION step)
  2025. // 2. Or if the target ViewBox is the same (=> NORMAL step)
  2026. //
  2027. // A change of ViewBox target between steps means the user is triggering
  2028. // the zoom fast (like a big scroll with its mousewheel)
  2029. //
  2030. // The new animation step with the new target will always take precedence over the
  2031. // last one and start from 0 (we overwrite zoomAnimStartTime and cancel the scheduled frame)
  2032. //
  2033. // So if we don't detect the change of target and adapt our computation,
  2034. // the user will see a delay at beginning the ratio will stays at 0 for some frames
  2035. //
  2036. // Hence when detecting the change of target, we animate from the previous target.
  2037. //
  2038. // The next step will then take the lead and continue from there, achieving a nicer
  2039. // experience for user.
  2040. // Change of target IF: an old animation start value exists AND the target has actually changed
  2041. if (oldZoomAnimStartTime && self.zoomAnimCVBTarget && self.zoomAnimCVBTarget.w !== targetW) {
  2042. // Compute the real time elapsed with the last step
  2043. var realElapsed = (new Date()).getTime() - oldZoomAnimStartTime;
  2044. // Compute then the actual ratio we're at
  2045. var realRatio = easingFormula(realElapsed / duration);
  2046. // Compute new ViewBox values
  2047. // The difference with the normal function is regarding the delta value used
  2048. // We don't take the current (dx, dy, dw, dh) values yet because they are related to the new target
  2049. // But we take the old target
  2050. x = cx + (self.zoomAnimCVBTarget.x - cx) * realRatio;
  2051. y = cy + (self.zoomAnimCVBTarget.y - cy) * realRatio;
  2052. w = cw + (self.zoomAnimCVBTarget.w - cw) * realRatio;
  2053. h = ch + (self.zoomAnimCVBTarget.h - ch) * realRatio;
  2054. // Update cw, cy, cw and ch so the next step take animation from here
  2055. cx = x;
  2056. dx = targetX - cx;
  2057. cy = y;
  2058. dy = targetY - cy;
  2059. cw = w;
  2060. dw = targetW - cw;
  2061. ch = h;
  2062. dh = targetH - ch;
  2063. // Update the current ViewBox target
  2064. self.zoomAnimCVBTarget = {
  2065. x: targetX, y: targetY, w: targetW, h: targetH
  2066. };
  2067. } else {
  2068. // This is the classical approach when nothing come interrupting the zoom
  2069. // Compute ratio according to elasped time and easing formula
  2070. var ratio = easingFormula(elapsed / duration);
  2071. // From the current value, we add a delta with a ratio that will leads us to the target
  2072. x = cx + dx * ratio;
  2073. y = cy + dy * ratio;
  2074. w = cw + dw * ratio;
  2075. h = ch + dh * ratio;
  2076. }
  2077. // Some checks before applying the new viewBox
  2078. if (zoomDir === 'in' && (w > self.currentViewBox.w || w < targetW)) {
  2079. // Zooming IN and the new ViewBox seems larger than the current value, or smaller than target value
  2080. // We do NOT set the ViewBox with this value
  2081. // Otherwise, the user would see the camera going back and forth
  2082. } else if (zoomDir === 'out' && (w < self.currentViewBox.w || w > targetW)) {
  2083. // Zooming OUT and the new ViewBox seems smaller than the current value, or larger than target value
  2084. // We do NOT set the ViewBox with this value
  2085. // Otherwise, the user would see the camera going back and forth
  2086. } else {
  2087. // New values look good, applying
  2088. self.setViewBox(x, y, w, h);
  2089. }
  2090. // Schedule the next step
  2091. self.zoomAnimID = self.requestAnimationFrame(computeNextStep);
  2092. } else {
  2093. /* Zoom animation done ! */
  2094. // Perform some cleaning
  2095. self.zoomAnimStartTime = null;
  2096. self.zoomAnimCVBTarget = null;
  2097. // Make sure the ViewBox hits the target!
  2098. if (self.currentViewBox.w !== targetW) {
  2099. self.setViewBox(targetX, targetY, targetW, targetH);
  2100. }
  2101. // Finally trigger afterZoom event
  2102. self.$map.trigger("afterZoom", {
  2103. x1: targetX, y1: targetY,
  2104. x2: (targetX + targetW), y2: (targetY + targetH)
  2105. });
  2106. }
  2107. };
  2108. // Invoke the first step directly
  2109. computeNextStep();
  2110. },
  2111. /*
  2112. * requestAnimationFrame/cancelAnimationFrame polyfill
  2113. * Based on https://gist.github.com/jlmakes/47eba84c54bc306186ac1ab2ffd336d4
  2114. * and also https://gist.github.com/paulirish/1579671
  2115. *
  2116. * _requestAnimationFrameFn and _cancelAnimationFrameFn hold the current functions
  2117. * But requestAnimationFrame and cancelAnimationFrame shall be called since
  2118. * in order to be in window context
  2119. */
  2120. // The function to use for requestAnimationFrame
  2121. requestAnimationFrame: function(callback) {
  2122. return this._requestAnimationFrameFn.call(window, callback);
  2123. },
  2124. // The function to use for cancelAnimationFrame
  2125. cancelAnimationFrame: function(id) {
  2126. this._cancelAnimationFrameFn.call(window, id);
  2127. },
  2128. // The requestAnimationFrame polyfill'd function
  2129. // Value set by self-invoking function, will be run only once
  2130. _requestAnimationFrameFn: (function () {
  2131. var polyfill = (function () {
  2132. var clock = (new Date()).getTime();
  2133. return function (callback) {
  2134. var currentTime = (new Date()).getTime();
  2135. // requestAnimationFrame strive to run @60FPS
  2136. // (e.g. every 16 ms)
  2137. if (currentTime - clock > 16) {
  2138. clock = currentTime;
  2139. callback(currentTime);
  2140. } else {
  2141. // Ask browser to schedule next callback when possible
  2142. return setTimeout(function () {
  2143. polyfill(callback);
  2144. }, 0);
  2145. }
  2146. };
  2147. })();
  2148. return window.requestAnimationFrame ||
  2149. window.webkitRequestAnimationFrame ||
  2150. window.mozRequestAnimationFrame ||
  2151. window.msRequestAnimationFrame ||
  2152. window.oRequestAnimationFrame ||
  2153. polyfill;
  2154. })(),
  2155. // The CancelAnimationFrame polyfill'd function
  2156. // Value set by self-invoking function, will be run only once
  2157. _cancelAnimationFrameFn: (function () {
  2158. return window.cancelAnimationFrame ||
  2159. window.webkitCancelAnimationFrame ||
  2160. window.webkitCancelRequestAnimationFrame ||
  2161. window.mozCancelAnimationFrame ||
  2162. window.mozCancelRequestAnimationFrame ||
  2163. window.msCancelAnimationFrame ||
  2164. window.msCancelRequestAnimationFrame ||
  2165. window.oCancelAnimationFrame ||
  2166. window.oCancelRequestAnimationFrame ||
  2167. clearTimeout;
  2168. })(),
  2169. /*
  2170. * SetViewBox wrapper
  2171. * Apply new viewbox values and keep track of them
  2172. *
  2173. * This avoid using the internal variable paper._viewBox which
  2174. * may not be present in future version of Raphael
  2175. */
  2176. setViewBox: function(x, y, w, h) {
  2177. var self = this;
  2178. // Update current value
  2179. self.currentViewBox.x = x;
  2180. self.currentViewBox.y = y;
  2181. self.currentViewBox.w = w;
  2182. self.currentViewBox.h = h;
  2183. // Perform set view box
  2184. self.paper.setViewBox(x, y, w, h, false);
  2185. },
  2186. /*
  2187. * Animate wrapper for Raphael element
  2188. *
  2189. * Perform an animation and ensure the non-animated attr are set.
  2190. * This is needed for specific attributes like cursor who will not
  2191. * be animated, and thus not set.
  2192. *
  2193. * If duration is set to 0 (or not set), no animation are performed
  2194. * and attributes are directly set (and the callback directly called)
  2195. */
  2196. // List extracted from Raphael internal vars
  2197. // Diff between Raphael.availableAttrs and Raphael._availableAnimAttrs
  2198. _nonAnimatedAttrs: [
  2199. "arrow-end", "arrow-start", "gradient",
  2200. "class", "cursor", "text-anchor",
  2201. "font", "font-family", "font-style", "font-weight", "letter-spacing",
  2202. "src", "href", "target", "title",
  2203. "stroke-dasharray", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit"
  2204. ],
  2205. /*
  2206. * @param element Raphael element
  2207. * @param attrs Attributes object to animate
  2208. * @param duration Animation duration in ms
  2209. * @param callback Callback to eventually call after animation is done
  2210. */
  2211. animate: function(element, attrs, duration, callback) {
  2212. var self = this;
  2213. // Check element
  2214. if (!element) return;
  2215. if (duration > 0) {
  2216. // Filter out non-animated attributes
  2217. // Note: we don't need to delete from original attribute (they won't be set anyway)
  2218. var attrsNonAnimated = {};
  2219. for (var i=0 ; i < self._nonAnimatedAttrs.length ; i++) {
  2220. var attrName = self._nonAnimatedAttrs[i];
  2221. if (attrs[attrName] !== undefined) {
  2222. attrsNonAnimated[attrName] = attrs[attrName];
  2223. }
  2224. }
  2225. // Set non-animated attributes
  2226. element.attr(attrsNonAnimated);
  2227. // Start animation for all attributes
  2228. element.animate(attrs, duration, 'linear', function() {
  2229. if (callback) callback();
  2230. });
  2231. } else {
  2232. // No animation: simply set all attributes...
  2233. element.attr(attrs);
  2234. // ... and call the callback if needed
  2235. if (callback) callback();
  2236. }
  2237. },
  2238. /*
  2239. * Check for Raphael bug regarding drawing while beeing hidden (under display:none)
  2240. * See https://github.com/neveldo/jQuery-Mapael/issues/135
  2241. * @return true/false
  2242. *
  2243. * Wants to override this behavior? Use prototype overriding:
  2244. * $.mapael.prototype.isRaphaelBBoxBugPresent = function() {return false;};
  2245. */
  2246. isRaphaelBBoxBugPresent: function() {
  2247. var self = this;
  2248. // Draw text, then get its boundaries
  2249. var textElem = self.paper.text(-50, -50, "TEST");
  2250. var textElemBBox = textElem.getBBox();
  2251. // remove element
  2252. textElem.remove();
  2253. // If it has no height and width, then the paper is hidden
  2254. return (textElemBBox.width === 0 && textElemBBox.height === 0);
  2255. },
  2256. // Default map options
  2257. defaultOptions: {
  2258. map: {
  2259. cssClass: "map",
  2260. tooltip: {
  2261. cssClass: "mapTooltip"
  2262. },
  2263. defaultArea: {
  2264. attrs: {
  2265. fill: "#343434",
  2266. stroke: "#5d5d5d",
  2267. "stroke-width": 1,
  2268. "stroke-linejoin": "round"
  2269. },
  2270. attrsHover: {
  2271. fill: "#f38a03",
  2272. animDuration: 300
  2273. },
  2274. text: {
  2275. position: "inner",
  2276. margin: 10,
  2277. attrs: {
  2278. "font-size": 15,
  2279. fill: "#c7c7c7"
  2280. },
  2281. attrsHover: {
  2282. fill: "#eaeaea",
  2283. "animDuration": 300
  2284. }
  2285. },
  2286. target: "_self",
  2287. cssClass: "area"
  2288. },
  2289. defaultPlot: {
  2290. type: "circle",
  2291. size: 15,
  2292. attrs: {
  2293. fill: "#0088db",
  2294. stroke: "#fff",
  2295. "stroke-width": 0,
  2296. "stroke-linejoin": "round"
  2297. },
  2298. attrsHover: {
  2299. "stroke-width": 3,
  2300. animDuration: 300
  2301. },
  2302. text: {
  2303. position: "right",
  2304. margin: 10,
  2305. attrs: {
  2306. "font-size": 15,
  2307. fill: "#c7c7c7"
  2308. },
  2309. attrsHover: {
  2310. fill: "#eaeaea",
  2311. animDuration: 300
  2312. }
  2313. },
  2314. target: "_self",
  2315. cssClass: "plot"
  2316. },
  2317. defaultLink: {
  2318. factor: 0.5,
  2319. attrs: {
  2320. stroke: "#0088db",
  2321. "stroke-width": 2
  2322. },
  2323. attrsHover: {
  2324. animDuration: 300
  2325. },
  2326. text: {
  2327. position: "inner",
  2328. margin: 10,
  2329. attrs: {
  2330. "font-size": 15,
  2331. fill: "#c7c7c7"
  2332. },
  2333. attrsHover: {
  2334. fill: "#eaeaea",
  2335. animDuration: 300
  2336. }
  2337. },
  2338. target: "_self",
  2339. cssClass: "link"
  2340. },
  2341. zoom: {
  2342. enabled: false,
  2343. minLevel: 0,
  2344. maxLevel: 10,
  2345. step: 0.25,
  2346. mousewheel: true,
  2347. touch: true,
  2348. animDuration: 200,
  2349. animEasing: "linear",
  2350. buttons: {
  2351. "reset": {
  2352. cssClass: "zoomButton zoomReset",
  2353. content: "&#8226;", // bullet sign
  2354. title: "Reset zoom"
  2355. },
  2356. "in": {
  2357. cssClass: "zoomButton zoomIn",
  2358. content: "+",
  2359. title: "Zoom in"
  2360. },
  2361. "out": {
  2362. cssClass: "zoomButton zoomOut",
  2363. content: "&#8722;", // minus sign
  2364. title: "Zoom out"
  2365. }
  2366. }
  2367. }
  2368. },
  2369. legend: {
  2370. redrawOnResize: true,
  2371. area: [],
  2372. plot: []
  2373. },
  2374. areas: {},
  2375. plots: {},
  2376. links: {}
  2377. },
  2378. // Default legends option
  2379. legendDefaultOptions: {
  2380. area: {
  2381. cssClass: "areaLegend",
  2382. display: true,
  2383. marginLeft: 10,
  2384. marginLeftTitle: 5,
  2385. marginBottomTitle: 10,
  2386. marginLeftLabel: 10,
  2387. marginBottom: 10,
  2388. titleAttrs: {
  2389. "font-size": 16,
  2390. fill: "#343434",
  2391. "text-anchor": "start"
  2392. },
  2393. labelAttrs: {
  2394. "font-size": 12,
  2395. fill: "#343434",
  2396. "text-anchor": "start"
  2397. },
  2398. labelAttrsHover: {
  2399. fill: "#787878",
  2400. animDuration: 300
  2401. },
  2402. hideElemsOnClick: {
  2403. enabled: true,
  2404. opacity: 0.2,
  2405. animDuration: 300
  2406. },
  2407. slices: [],
  2408. mode: "vertical"
  2409. },
  2410. plot: {
  2411. cssClass: "plotLegend",
  2412. display: true,
  2413. marginLeft: 10,
  2414. marginLeftTitle: 5,
  2415. marginBottomTitle: 10,
  2416. marginLeftLabel: 10,
  2417. marginBottom: 10,
  2418. titleAttrs: {
  2419. "font-size": 16,
  2420. fill: "#343434",
  2421. "text-anchor": "start"
  2422. },
  2423. labelAttrs: {
  2424. "font-size": 12,
  2425. fill: "#343434",
  2426. "text-anchor": "start"
  2427. },
  2428. labelAttrsHover: {
  2429. fill: "#787878",
  2430. animDuration: 300
  2431. },
  2432. hideElemsOnClick: {
  2433. enabled: true,
  2434. opacity: 0.2,
  2435. animDuration: 300
  2436. },
  2437. slices: [],
  2438. mode: "vertical"
  2439. }
  2440. }
  2441. };
  2442. // Mapael version number
  2443. // Accessible as $.mapael.version
  2444. Mapael.version = version;
  2445. // Extend jQuery with Mapael
  2446. if ($[pluginName] === undefined) $[pluginName] = Mapael;
  2447. // Add jQuery DOM function
  2448. $.fn[pluginName] = function (options) {
  2449. // Call Mapael on each element
  2450. return this.each(function () {
  2451. // Avoid leaking problem on multiple instanciation by removing an old mapael object on a container
  2452. if ($.data(this, pluginName)) {
  2453. $.data(this, pluginName).destroy();
  2454. }
  2455. // Create Mapael and save it as jQuery data
  2456. // This allow external access to Mapael using $(".mapcontainer").data("mapael")
  2457. $.data(this, pluginName, new Mapael(this, options));
  2458. });
  2459. };
  2460. return Mapael;
  2461. }));