You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

jquery.mapael.js 120KB

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