mapael.js 98 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249
  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.0.0
  7. *
  8. * Copyright (c) 2015 Vincent Brouté (http://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('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.0.0-dev";
  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. // Save initial HTML content (used by destroy method)
  46. self.initialHTMLContent = self.$container.html();
  47. // zoom TimeOut handler (used to set and clear)
  48. self.zoomTO = 0;
  49. // zoom center coordinate (set at touchstart)
  50. self.zoomCenterX = 0;
  51. self.zoomCenterY = 0;
  52. // Zoom pinch (set at touchstart and touchmove)
  53. self.previousPinchDist = 0;
  54. // Zoom data
  55. self.zoomData = {
  56. zoomLevel: 0,
  57. zoomX: 0,
  58. zoomY: 0,
  59. panX: 0,
  60. panY: 0
  61. };
  62. // resize TimeOut handler (used to set and clear)
  63. self.resizeTO = 0;
  64. // Panning: tell if panning action is in progress
  65. self.panning = false;
  66. // Panning TimeOut handler (used to set and clear)
  67. self.panningTO = 0;
  68. // Animate view box Interval handler (used to set and clear)
  69. self.animationIntervalID = null;
  70. // Map subcontainer jQuery object
  71. self.$map = {};
  72. // The tooltip jQuery object
  73. self.$tooltip = {};
  74. // The paper Raphael object
  75. self.paper = {};
  76. // The areas object list
  77. self.areas = {};
  78. // The plots object list
  79. self.plots = {};
  80. // The links object list
  81. self.links = {};
  82. // The map configuration object (taken from map file)
  83. self.mapConf = {};
  84. // Let's start the initialization
  85. self.init();
  86. };
  87. /*
  88. * Mapael Prototype
  89. * Defines all methods and properties needed by Mapael
  90. * Each mapael object inherits their properties and methods from this prototype
  91. */
  92. Mapael.prototype = {
  93. /*
  94. * Version number
  95. */
  96. version: version,
  97. /*
  98. * Initialize the plugin
  99. * Called by the constructor
  100. */
  101. init: function () {
  102. var self = this;
  103. // Init check for class existence
  104. if (self.options.map.cssClass === "" || $("." + self.options.map.cssClass, self.container).length === 0) {
  105. throw new Error("The map class `" + self.options.map.cssClass + "` doesn't exists");
  106. }
  107. // Create the tooltip container
  108. self.$tooltip = $("<div>").addClass(self.options.map.tooltip.cssClass).css("display", "none");
  109. // Get the map container, empty it then append tooltip
  110. self.$map = $("." + self.options.map.cssClass, self.container).empty().append(self.$tooltip);
  111. // Get the map from $.mapael or $.fn.mapael (backward compatibility)
  112. if ($[pluginName] && $[pluginName].maps && $[pluginName].maps[self.options.map.name]) {
  113. // Mapael version >= 2.x
  114. self.mapConf = $[pluginName].maps[self.options.map.name];
  115. } else if ($.fn[pluginName] && $.fn[pluginName].maps && $.fn[pluginName].maps[self.options.map.name]) {
  116. // Mapael version <= 1.x - DEPRECATED
  117. self.mapConf = $.fn[pluginName].maps[self.options.map.name];
  118. if (window.console && window.console.warn) {
  119. window.console.warn("Extending $.fn.mapael is deprecated (map '" + self.options.map.name + "')");
  120. }
  121. } else {
  122. throw new Error("Unknown map '" + self.options.map.name + "'");
  123. }
  124. // Create Raphael paper
  125. self.paper = new Raphael(self.$map[0], self.mapConf.width, self.mapConf.height);
  126. // issue #135: Check for Raphael bug on text element boundaries
  127. if (self.isRaphaelBBoxBugPresent() === true) {
  128. self.destroy();
  129. throw new Error("Can't get boundary box for text (is your container hidden? See #135)");
  130. }
  131. // add plugin class name on element
  132. self.$container.addClass(pluginName);
  133. if (self.options.map.tooltip.css) self.$tooltip.css(self.options.map.tooltip.css);
  134. self.paper.setViewBox(0, 0, self.mapConf.width, self.mapConf.height, false);
  135. // Handle map size
  136. if (self.options.map.width) {
  137. // NOT responsive: map has a fixed width
  138. self.paper.setSize(self.options.map.width, self.mapConf.height * (self.options.map.width / self.mapConf.width));
  139. // Create the legends for plots taking into account the scale of the map
  140. self.createLegends("plot", self.plots, (self.options.map.width / self.mapConf.width));
  141. } else {
  142. // Responsive: handle resizing of the map
  143. self.handleMapResizing();
  144. }
  145. // Draw map areas
  146. $.each(self.mapConf.elems, function (id) {
  147. var elemOptions = self.getElemOptions(
  148. self.options.map.defaultArea,
  149. (self.options.areas[id] ? self.options.areas[id] : {}),
  150. self.options.legend.area
  151. );
  152. self.areas[id] = {"mapElem": self.paper.path(self.mapConf.elems[id]).attr(elemOptions.attrs)};
  153. });
  154. // Hook that allows to add custom processing on the map
  155. if (self.options.map.beforeInit) self.options.map.beforeInit(self.$container, self.paper, self.options);
  156. // Init map areas in a second loop (prevent texts to be hidden by map elements)
  157. $.each(self.mapConf.elems, function (id) {
  158. var elemOptions = self.getElemOptions(
  159. self.options.map.defaultArea,
  160. (self.options.areas[id] ? self.options.areas[id] : {}),
  161. self.options.legend.area
  162. );
  163. self.initElem(self.areas[id], elemOptions, id);
  164. });
  165. // Draw links
  166. self.links = self.drawLinksCollection(self.options.links);
  167. // Draw plots
  168. $.each(self.options.plots, function (id) {
  169. self.plots[id] = self.drawPlot(id);
  170. });
  171. // Attach zoom event
  172. self.$container.on("zoom." + pluginName, function (e, zoomOptions) {
  173. self.onZoomEvent(e, zoomOptions);
  174. });
  175. if (self.options.map.zoom.enabled) {
  176. // Enable zoom
  177. self.initZoom(self.mapConf.width, self.mapConf.height, self.options.map.zoom);
  178. }
  179. // Set initial zoom
  180. if (self.options.map.zoom.init !== undefined) {
  181. if (self.options.map.zoom.init.animDuration === undefined) {
  182. self.options.map.zoom.init.animDuration = 0;
  183. }
  184. self.$container.trigger("zoom." + pluginName, self.options.map.zoom.init);
  185. }
  186. // Create the legends for areas
  187. self.createLegends("area", self.areas, 1);
  188. // Attach update event
  189. self.$container.on("update." + pluginName, function (e, opt) {
  190. self.onUpdateEvent(e, opt);
  191. });
  192. // Attach showElementsInRange event
  193. self.$container.on("showElementsInRange." + pluginName, function (e, opt) {
  194. self.onShowElementsInRange(e, opt);
  195. });
  196. // Hook that allows to add custom processing on the map
  197. if (self.options.map.afterInit) self.options.map.afterInit(self.$container, self.paper, self.areas, self.plots, self.options);
  198. $(self.paper.desc).append(" and Mapael " + self.version + " (http://www.vincentbroute.fr/mapael/)");
  199. },
  200. /*
  201. * Destroy mapael
  202. * This function effectively detach mapael from the container
  203. * - Set the container back to the way it was before mapael instanciation
  204. * - Remove all data associated to it (memory can then be free'ed by browser)
  205. *
  206. * This method can be call directly by user:
  207. * $(".mapcontainer").data("mapael").destroy();
  208. *
  209. * This method is also automatically called if the user try to call mapael
  210. * on a container already containing a mapael instance
  211. */
  212. destroy: function () {
  213. var self = this;
  214. // Detach all event listeners attached to the container
  215. self.$container.off("." + pluginName);
  216. // Empty the container (this will also detach all event listeners)
  217. self.$container.empty();
  218. // Detach the global resize event handler
  219. if (self.onResizeEvent) $(window).off("resize." + pluginName, self.onResizeEvent);
  220. // Replace initial HTML content
  221. self.$container.html(self.initialHTMLContent);
  222. // Remove mapael class
  223. self.$container.removeClass(pluginName);
  224. // Remove the data
  225. self.$container.removeData(pluginName);
  226. // Remove all internal reference
  227. self.container = undefined;
  228. self.$container = undefined;
  229. self.options = undefined;
  230. self.paper = undefined;
  231. self.$map = undefined;
  232. self.$tooltip = undefined;
  233. self.mapConf = undefined;
  234. self.areas = undefined;
  235. self.plots = undefined;
  236. self.links = undefined;
  237. },
  238. handleMapResizing: function () {
  239. var self = this;
  240. // onResizeEvent: call when the window element trigger the resize event
  241. // We create it inside this function (and not in the prototype) in order to have a closure
  242. // Otherwise, in the prototype, 'this' when triggered is *not* the mapael object but the global window
  243. self.onResizeEvent = function () {
  244. // Clear any previous setTimeout (avoid too much triggering)
  245. clearTimeout(self.resizeTO);
  246. // setTimeout to wait for the user to finish its resizing
  247. self.resizeTO = setTimeout(function () {
  248. self.$map.trigger("resizeEnd." + pluginName);
  249. }, 150);
  250. };
  251. // Attach resize handler
  252. $(window).on("resize." + pluginName, self.onResizeEvent);
  253. // Attach resize end handler, and call it once
  254. self.$map.on("resizeEnd." + pluginName, function () {
  255. var containerWidth = self.$map.width();
  256. if (self.paper.width != containerWidth) {
  257. var newScale = containerWidth / self.mapConf.width;
  258. // Set new size
  259. self.paper.setSize(containerWidth, self.mapConf.height * newScale);
  260. // Create plots legend again to take into account the new scale
  261. self.createLegends("plot", self.plots, newScale);
  262. }
  263. }).trigger("resizeEnd." + pluginName);
  264. },
  265. /*
  266. * Extend the user option with the default one
  267. * @param options the user options
  268. * @return new options object
  269. */
  270. extendDefaultOptions: function (options) {
  271. // Extend default options with user options
  272. options = $.extend(true, {}, Mapael.prototype.defaultOptions, options);
  273. // Extend legend default options
  274. $.each(options.legend, function (type) {
  275. if ($.isArray(options.legend[type])) {
  276. for (var i = 0; i < options.legend[type].length; ++i)
  277. options.legend[type][i] = $.extend(true, {}, Mapael.prototype.legendDefaultOptions[type], options.legend[type][i]);
  278. } else {
  279. options.legend[type] = $.extend(true, {}, Mapael.prototype.legendDefaultOptions[type], options.legend[type]);
  280. }
  281. });
  282. return options;
  283. },
  284. /*
  285. * Init the element "elem" on the map (drawing, setting attributes, events, tooltip, ...)
  286. */
  287. initElem: function (elem, elemOptions, id) {
  288. var self = this;
  289. var bbox = {};
  290. var textPosition = {};
  291. // Assign value attribute to element
  292. if (elemOptions.value !== undefined){
  293. elem.value = elemOptions.value;
  294. }
  295. // Init the label related to the element
  296. if (elemOptions.text && elemOptions.text.content !== undefined) {
  297. // Set a text label in the area
  298. bbox = elem.mapElem.getBBox();
  299. textPosition = self.getTextPosition(bbox, elemOptions.text.position, elemOptions.text.margin);
  300. elemOptions.text.attrs["text-anchor"] = textPosition.textAnchor;
  301. elem.textElem = self.paper.text(textPosition.x, textPosition.y, elemOptions.text.content).attr(elemOptions.text.attrs);
  302. $(elem.textElem.node).attr("data-id", id);
  303. }
  304. // Set user event handlers
  305. if (elemOptions.eventHandlers) self.setEventHandlers(id, elemOptions, elem.mapElem, elem.textElem);
  306. // Set hover option for mapElem
  307. self.setHoverOptions(elem.mapElem, elemOptions.attrs, elemOptions.attrsHover);
  308. // Set hover option for textElem
  309. if (elem.textElem) self.setHoverOptions(elem.textElem, elemOptions.text.attrs, elemOptions.text.attrsHover);
  310. // Set hover behavior only if attrsHover is set for area or for text
  311. if (($.isEmptyObject(elemOptions.attrsHover) === false) ||
  312. (elem.textElem && $.isEmptyObject(elemOptions.text.attrsHover) === false)) {
  313. // Set hover behavior
  314. self.setHover(elem.mapElem, elem.textElem);
  315. }
  316. // Init the tooltip
  317. if (elemOptions.tooltip) {
  318. elem.mapElem.tooltip = elemOptions.tooltip;
  319. self.setTooltip(elem.mapElem);
  320. if (elemOptions.text && elemOptions.text.content !== undefined) {
  321. elem.textElem.tooltip = elemOptions.tooltip;
  322. self.setTooltip(elem.textElem);
  323. }
  324. }
  325. // Init the link
  326. if (elemOptions.href) {
  327. elem.mapElem.href = elemOptions.href;
  328. elem.mapElem.target = elemOptions.target;
  329. self.setHref(elem.mapElem);
  330. if (elemOptions.text && elemOptions.text.content !== undefined) {
  331. elem.textElem.href = elemOptions.href;
  332. elem.textElem.target = elemOptions.target;
  333. self.setHref(elem.textElem);
  334. }
  335. }
  336. $(elem.mapElem.node).attr("data-id", id);
  337. },
  338. /*
  339. * Init zoom and panning for the map
  340. * @param mapWidth
  341. * @param mapHeight
  342. * @param zoomOptions
  343. */
  344. initZoom: function (mapWidth, mapHeight, zoomOptions) {
  345. var self = this;
  346. var mousedown = false;
  347. var previousX = 0;
  348. var previousY = 0;
  349. var fnZoomButtons = {
  350. "reset": function () {
  351. self.$container.trigger("zoom." + pluginName, {"level": 0});
  352. },
  353. "in": function () {
  354. self.$container.trigger("zoom." + pluginName, {"level": "+1"});
  355. },
  356. "out": function () {
  357. self.$container.trigger("zoom." + pluginName, {"level": -1});
  358. }
  359. };
  360. // init Zoom data
  361. $.extend(self.zoomData, {
  362. zoomLevel: 0,
  363. panX: 0,
  364. panY: 0
  365. });
  366. // init zoom buttons
  367. $.each(zoomOptions.buttons, function(type, opt) {
  368. if (fnZoomButtons[type] === undefined) throw new Error("Unknown zoom button '" + type + "'");
  369. // Create div with classes, contents and title (for tooltip)
  370. var $button = $("<div>").addClass(opt.cssClass)
  371. .html(opt.content)
  372. .attr("title", opt.title);
  373. // Assign click event
  374. $button.on("click." + pluginName, fnZoomButtons[type]);
  375. // Append to map
  376. self.$map.append($button);
  377. });
  378. // Update the zoom level of the map on mousewheel
  379. if (self.options.map.zoom.mousewheel) {
  380. self.$map.on("mousewheel." + pluginName, function (e) {
  381. var zoomLevel = (e.deltaY > 0) ? 1 : -1;
  382. var coord = self.mapPagePositionToXY(e.pageX, e.pageY);
  383. self.$container.trigger("zoom." + pluginName, {
  384. "fixedCenter": true,
  385. "level": self.zoomData.zoomLevel + zoomLevel,
  386. "x": coord.x,
  387. "y": coord.y
  388. });
  389. return false;
  390. });
  391. }
  392. // Update the zoom level of the map on touch pinch
  393. if (self.options.map.zoom.touch) {
  394. self.$map.on("touchstart." + pluginName, function (e) {
  395. if (e.originalEvent.touches.length === 2) {
  396. self.zoomCenterX = (e.originalEvent.touches[0].pageX + e.originalEvent.touches[1].pageX) / 2;
  397. self.zoomCenterY = (e.originalEvent.touches[0].pageY + e.originalEvent.touches[1].pageY) / 2;
  398. 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));
  399. }
  400. });
  401. self.$map.on("touchmove." + pluginName, function (e) {
  402. var pinchDist = 0;
  403. var zoomLevel = 0;
  404. if (e.originalEvent.touches.length === 2) {
  405. 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));
  406. if (Math.abs(pinchDist - self.previousPinchDist) > 15) {
  407. var coord = self.mapPagePositionToXY(self.zoomCenterX, self.zoomCenterY);
  408. zoomLevel = (pinchDist - self.previousPinchDist) / Math.abs(pinchDist - self.previousPinchDist);
  409. self.$container.trigger("zoom." + pluginName, {
  410. "fixedCenter": true,
  411. "level": self.zoomData.zoomLevel + zoomLevel,
  412. "x": coord.x,
  413. "y": coord.y
  414. });
  415. self.previousPinchDist = pinchDist;
  416. }
  417. return false;
  418. }
  419. });
  420. }
  421. // Panning
  422. $("body").on("mouseup." + pluginName + (zoomOptions.touch ? " touchend" : ""), function () {
  423. mousedown = false;
  424. setTimeout(function () {
  425. self.panning = false;
  426. }, 50);
  427. });
  428. self.$map.on("mousedown." + pluginName + (zoomOptions.touch ? " touchstart" : ""), function (e) {
  429. if (e.pageX !== undefined) {
  430. mousedown = true;
  431. previousX = e.pageX;
  432. previousY = e.pageY;
  433. } else {
  434. if (e.originalEvent.touches.length === 1) {
  435. mousedown = true;
  436. previousX = e.originalEvent.touches[0].pageX;
  437. previousY = e.originalEvent.touches[0].pageY;
  438. }
  439. }
  440. }).on("mousemove." + pluginName + (zoomOptions.touch ? " touchmove" : ""), function (e) {
  441. var currentLevel = self.zoomData.zoomLevel;
  442. var pageX = 0;
  443. var pageY = 0;
  444. if (e.pageX !== undefined) {
  445. pageX = e.pageX;
  446. pageY = e.pageY;
  447. } else {
  448. if (e.originalEvent.touches.length === 1) {
  449. pageX = e.originalEvent.touches[0].pageX;
  450. pageY = e.originalEvent.touches[0].pageY;
  451. } else {
  452. mousedown = false;
  453. }
  454. }
  455. if (mousedown && currentLevel !== 0) {
  456. var offsetX = (previousX - pageX) / (1 + (currentLevel * zoomOptions.step)) * (mapWidth / self.paper.width);
  457. var offsetY = (previousY - pageY) / (1 + (currentLevel * zoomOptions.step)) * (mapHeight / self.paper.height);
  458. var panX = Math.min(Math.max(0, self.paper._viewBox[0] + offsetX), (mapWidth - self.paper._viewBox[2]));
  459. var panY = Math.min(Math.max(0, self.paper._viewBox[1] + offsetY), (mapHeight - self.paper._viewBox[3]));
  460. if (Math.abs(offsetX) > 5 || Math.abs(offsetY) > 5) {
  461. $.extend(self.zoomData, {
  462. panX: panX,
  463. panY: panY,
  464. zoomX: panX + self.paper._viewBox[2] / 2,
  465. zoomY: panY + self.paper._viewBox[3] / 2
  466. });
  467. self.paper.setViewBox(panX, panY, self.paper._viewBox[2], self.paper._viewBox[3]);
  468. clearTimeout(self.panningTO);
  469. self.panningTO = setTimeout(function () {
  470. self.$map.trigger("afterPanning", {
  471. x1: panX,
  472. y1: panY,
  473. x2: (panX + self.paper._viewBox[2]),
  474. y2: (panY + self.paper._viewBox[3])
  475. });
  476. }, 150);
  477. previousX = pageX;
  478. previousY = pageY;
  479. self.panning = true;
  480. }
  481. return false;
  482. }
  483. });
  484. },
  485. /*
  486. * Map a mouse position to a map position
  487. * Transformation principle:
  488. * ** start with (pageX, pageY) absolute mouse coordinate
  489. * - Apply translation: take into accounts the map offset in the page
  490. * ** from this point, we have relative mouse coordinate
  491. * - Apply homothetic transformation: take into accounts initial factor of map sizing (fullWidth / actualWidth)
  492. * - Apply homothetic transformation: take into accounts the zoom factor
  493. * ** from this point, we have relative map coordinate
  494. * - Apply translation: take into accounts the current panning of the map
  495. * ** from this point, we have absolute map coordinate
  496. * @param pageX: mouse client coordinate on X
  497. * @param pageY: mouse client coordinate on Y
  498. * @return map coordinate {x, y}
  499. */
  500. mapPagePositionToXY: function(pageX, pageY) {
  501. var self = this;
  502. var offset = self.$map.offset();
  503. var initFactor = (self.options.map.width) ? (self.mapConf.width / self.options.map.width) : (self.mapConf.width / self.$map.width());
  504. var zoomFactor = 1 / (1 + (self.zoomData.zoomLevel * self.options.map.zoom.step));
  505. return {
  506. x: (zoomFactor * initFactor * (pageX - offset.left)) + self.zoomData.panX,
  507. y: (zoomFactor * initFactor * (pageY - offset.top)) + self.zoomData.panY
  508. };
  509. },
  510. /*
  511. * Zoom on the map at a specific level focused on specific coordinates
  512. * If no coordinates are specified, the zoom will be focused on the center of the map
  513. * options :
  514. * "level" : level of the zoom between minLevel and maxLevel
  515. * "x" or "latitude" : x coordinate or latitude of the point to focus on
  516. * "y" or "longitude" : y coordinate or longitude of the point to focus on
  517. * "fixedCenter" : set to true in order to preserve the position of x,y in the canvas when zoomed
  518. * "animDuration" : zoom duration
  519. */
  520. onZoomEvent: function (e, zoomOptions) {
  521. var self = this;
  522. var newLevel = self.zoomData.zoomLevel;
  523. var panX = 0;
  524. var panY = 0;
  525. var previousZoomLevel = (1 + self.zoomData.zoomLevel * self.options.map.zoom.step);
  526. var zoomLevel = 0;
  527. var animDuration = (zoomOptions.animDuration !== undefined) ? zoomOptions.animDuration : self.options.map.zoom.animDuration;
  528. var offsetX = 0;
  529. var offsetY = 0;
  530. var coords = {};
  531. // Get user defined zoom level
  532. if (zoomOptions.level !== undefined) {
  533. if (typeof zoomOptions.level === "string") {
  534. // level is a string, either "n", "+n" or "-n"
  535. if ((zoomOptions.level.slice(0, 1) === '+') || (zoomOptions.level.slice(0, 1) === '-')) {
  536. // zoomLevel is relative
  537. newLevel = self.zoomData.zoomLevel + parseInt(zoomOptions.level);
  538. } else {
  539. // zoomLevel is absolute
  540. newLevel = parseInt(zoomOptions.level);
  541. }
  542. } else {
  543. // level is integer
  544. if (zoomOptions.level < 0) {
  545. // zoomLevel is relative
  546. newLevel = self.zoomData.zoomLevel + zoomOptions.level;
  547. } else {
  548. // zoomLevel is absolute
  549. newLevel = zoomOptions.level;
  550. }
  551. }
  552. // Make sure we stay in the boundaries
  553. newLevel = Math.min(Math.max(newLevel, self.options.map.zoom.minLevel), self.options.map.zoom.maxLevel);
  554. }
  555. zoomLevel = (1 + newLevel * self.options.map.zoom.step);
  556. if (zoomOptions.latitude !== undefined && zoomOptions.longitude !== undefined) {
  557. coords = self.mapConf.getCoords(zoomOptions.latitude, zoomOptions.longitude);
  558. zoomOptions.x = coords.x;
  559. zoomOptions.y = coords.y;
  560. }
  561. if (zoomOptions.x === undefined)
  562. zoomOptions.x = self.paper._viewBox[0] + self.paper._viewBox[2] / 2;
  563. if (zoomOptions.y === undefined)
  564. zoomOptions.y = (self.paper._viewBox[1] + self.paper._viewBox[3] / 2);
  565. if (newLevel === 0) {
  566. panX = 0;
  567. panY = 0;
  568. } else if (zoomOptions.fixedCenter !== undefined && zoomOptions.fixedCenter === true) {
  569. offsetX = self.zoomData.panX + ((zoomOptions.x - self.zoomData.panX) * (zoomLevel - previousZoomLevel)) / zoomLevel;
  570. offsetY = self.zoomData.panY + ((zoomOptions.y - self.zoomData.panY) * (zoomLevel - previousZoomLevel)) / zoomLevel;
  571. panX = Math.min(Math.max(0, offsetX), (self.mapConf.width - (self.mapConf.width / zoomLevel)));
  572. panY = Math.min(Math.max(0, offsetY), (self.mapConf.height - (self.mapConf.height / zoomLevel)));
  573. } else {
  574. panX = Math.min(Math.max(0, zoomOptions.x - (self.mapConf.width / zoomLevel) / 2), (self.mapConf.width - (self.mapConf.width / zoomLevel)));
  575. panY = Math.min(Math.max(0, zoomOptions.y - (self.mapConf.height / zoomLevel) / 2), (self.mapConf.height - (self.mapConf.height / zoomLevel)));
  576. }
  577. // Update zoom level of the map
  578. if (zoomLevel == previousZoomLevel && panX == self.zoomData.panX && panY == self.zoomData.panY) return;
  579. if (animDuration > 0) {
  580. self.animateViewBox(panX, panY, self.mapConf.width / zoomLevel, self.mapConf.height / zoomLevel, animDuration, self.options.map.zoom.animEasing);
  581. } else {
  582. self.paper.setViewBox(panX, panY, self.mapConf.width / zoomLevel, self.mapConf.height / zoomLevel);
  583. clearTimeout(self.zoomTO);
  584. self.zoomTO = setTimeout(function () {
  585. self.$map.trigger("afterZoom", {
  586. x1: panX,
  587. y1: panY,
  588. x2: (panX + (self.mapConf.width / zoomLevel)),
  589. y2: (panY + (self.mapConf.height / zoomLevel))
  590. });
  591. }, 150);
  592. }
  593. $.extend(self.zoomData, {
  594. zoomLevel: newLevel,
  595. panX: panX,
  596. panY: panY,
  597. zoomX: panX + self.paper._viewBox[2] / 2,
  598. zoomY: panY + self.paper._viewBox[3] / 2
  599. });
  600. },
  601. /*
  602. * Show some element in range defined by user
  603. * Triggered by user $(".mapcontainer").trigger("showElementsInRange", [opt]);
  604. *
  605. * @param opt the options
  606. * opt.hiddenOpacity opacity for hidden element (default = 0.3)
  607. * opt.animDuration animation duration in ms (default = 0)
  608. * opt.afterShowRange callback
  609. * opt.ranges the range to show:
  610. * Example:
  611. * opt.ranges = {
  612. * 'plot' : {
  613. * 0 : { // valueIndex
  614. * 'min': 1000,
  615. * 'max': 1200
  616. * },
  617. * 1 : { // valueIndex
  618. * 'min': 10,
  619. * 'max': 12
  620. * }
  621. * },
  622. * 'area' : {
  623. * {'min': 10, 'max': 20} // No valueIndex, only an object, use 0 as valueIndex (easy case)
  624. * }
  625. * }
  626. */
  627. onShowElementsInRange: function(e, opt) {
  628. var self = this;
  629. // set animDuration to default if not defined
  630. if (opt.animDuration === undefined) {
  631. opt.animDuration = 0;
  632. }
  633. // set hiddenOpacity to default if not defined
  634. if (opt.hiddenOpacity === undefined) {
  635. opt.hiddenOpacity = 0.3;
  636. }
  637. // handle area
  638. if (opt.ranges && opt.ranges.area) {
  639. self.showElemByRange(opt.ranges.area, self.areas, opt.hiddenOpacity, opt.animDuration);
  640. }
  641. // handle plot
  642. if (opt.ranges && opt.ranges.plot) {
  643. self.showElemByRange(opt.ranges.plot, self.plots, opt.hiddenOpacity, opt.animDuration);
  644. }
  645. // handle link
  646. if (opt.ranges && opt.ranges.link) {
  647. self.showElemByRange(opt.ranges.link, self.links, opt.hiddenOpacity, opt.animDuration);
  648. }
  649. // Call user callback
  650. if (opt.afterShowRange) opt.afterShowRange();
  651. },
  652. /*
  653. * Show some element in range
  654. * @param ranges: the ranges
  655. * @param elems: list of element on which to check against previous range
  656. * @hiddenOpacity: the opacity when hidden
  657. * @animDuration: the animation duration
  658. */
  659. showElemByRange: function(ranges, elems, hiddenOpacity, animDuration) {
  660. var self = this;
  661. // Hold the final opacity value for all elements consolidated after applying each ranges
  662. // This allow to set the opacity only once for each elements
  663. var elemsFinalOpacity = {};
  664. // set object with one valueIndex to 0 if we have directly the min/max
  665. if (ranges.min !== undefined || ranges.max !== undefined) {
  666. ranges = {0: ranges};
  667. }
  668. // Loop through each valueIndex
  669. $.each(ranges, function (valueIndex) {
  670. var range = ranges[valueIndex];
  671. // Check if user defined at least a min or max value
  672. if (range.min === undefined && range.max === undefined) {
  673. return true; // skip this iteration (each loop), goto next range
  674. }
  675. // Loop through each elements
  676. $.each(elems, function (id) {
  677. var elemValue = elems[id].value;
  678. // set value with one valueIndex to 0 if not object
  679. if (typeof elemValue !== "object") {
  680. elemValue = [elemValue];
  681. }
  682. // Check existence of this value index
  683. if (elemValue[valueIndex] === undefined) {
  684. return true; // skip this iteration (each loop), goto next element
  685. }
  686. // Check if in range
  687. if ((range.min !== undefined && elemValue[valueIndex] < range.min) ||
  688. (range.max !== undefined && elemValue[valueIndex] > range.max)) {
  689. // Element not in range
  690. elemsFinalOpacity[id] = hiddenOpacity;
  691. } else {
  692. // Element in range
  693. elemsFinalOpacity[id] = 1;
  694. }
  695. });
  696. });
  697. // Now that we looped through all ranges, we can really assign the final opacity
  698. $.each(elemsFinalOpacity, function (id) {
  699. self.setElementOpacity(elems[id], elemsFinalOpacity[id], animDuration);
  700. });
  701. },
  702. /*
  703. * Set element opacity
  704. * Handle elem.mapElem and elem.textElem
  705. * @param elem the element
  706. * @param opacity the opacity to apply
  707. * @param animDuration the animation duration to use
  708. */
  709. setElementOpacity: function(elem, opacity, animDuration) {
  710. // Ensure no animation is running
  711. //elem.mapElem.stop();
  712. //if (elem.textElem) elem.textElem.stop();
  713. // If final opacity is not null, ensure element is shown before proceeding
  714. if (opacity > 0) {
  715. elem.mapElem.show();
  716. if (elem.textElem) elem.textElem.show();
  717. }
  718. if (animDuration > 0) {
  719. // Animate attribute
  720. elem.mapElem.animate({"opacity": opacity}, animDuration, "linear", function () {
  721. // If final attribute is 0, hide
  722. if (opacity === 0) elem.mapElem.hide();
  723. });
  724. // Handle text element
  725. if (elem.textElem) {
  726. // Animate attribute
  727. elem.textElem.animate({"opacity": opacity}, animDuration, "linear", function () {
  728. // If final attribute is 0, hide
  729. if (opacity === 0) elem.textElem.hide();
  730. });
  731. }
  732. } else {
  733. // Set attribute
  734. elem.mapElem.attr({"opacity": opacity});
  735. // For null opacity, hide it
  736. if (opacity === 0) elem.mapElem.hide();
  737. // Handle text elemen
  738. if (elem.textElem) {
  739. // Set attribute
  740. elem.textElem.attr({"opacity": opacity});
  741. // For null opacity, hide it
  742. if (opacity === 0) elem.textElem.hide();
  743. }
  744. }
  745. },
  746. /*
  747. *
  748. * Update the current map
  749. * Refresh attributes and tooltips for areas and plots
  750. * @param opt option for the refresh :
  751. * opt.mapOptions: options to update for plots and areas
  752. * opt.replaceOptions: whether mapsOptions should entirely replace current map options, or just extend it
  753. * opt.opt.newPlots new plots to add to the map
  754. * opt.newLinks new links to add to the map
  755. * opt.deletePlotKeys plots to delete from the map (array, or "all" to remove all plots)
  756. * opt.deleteLinkKeys links to remove from the map (array, or "all" to remove all links)
  757. * opt.setLegendElemsState the state of legend elements to be set : show (default) or hide
  758. * opt.animDuration animation duration in ms (default = 0)
  759. * opt.afterUpdate Hook that allows to add custom processing on the map
  760. */
  761. onUpdateEvent: function (e, opt) {
  762. var self = this;
  763. // Abort if opt is undefined
  764. if (typeof opt !== "object") return;
  765. var i = 0;
  766. var animDuration = (opt.animDuration) ? opt.animDuration : 0;
  767. // This function remove an element using animation (or not, depending on animDuration)
  768. // Used for deletePlotKeys and deleteLinkKeys
  769. var fnRemoveElement = function (elem) {
  770. // Unset all event handlers
  771. self.unsetHover(elem.mapElem, elem.textElem);
  772. if (animDuration > 0) {
  773. elem.mapElem.animate({"opacity": 0}, animDuration, "linear", function () {
  774. elem.mapElem.remove();
  775. });
  776. if (elem.textElem) {
  777. elem.textElem.animate({"opacity": 0}, animDuration, "linear", function () {
  778. elem.textElem.remove();
  779. });
  780. }
  781. } else {
  782. elem.mapElem.remove();
  783. if (elem.textElem) {
  784. elem.textElem.remove();
  785. }
  786. }
  787. };
  788. // This function show an element using animation
  789. // Used for newPlots and newLinks
  790. var fnShowElement = function (elem) {
  791. // Starts with hidden elements
  792. elem.mapElem.attr({opacity: 0});
  793. if (elem.textElem) elem.textElem.attr({opacity: 0});
  794. // Set final element opacity
  795. self.setElementOpacity(
  796. elem,
  797. (elem.mapElem.originalAttrs.opacity !== undefined) ? elem.mapElem.originalAttrs.opacity : 1,
  798. animDuration
  799. );
  800. };
  801. if (typeof opt.mapOptions === "object") {
  802. if (opt.replaceOptions === true) self.options = self.extendDefaultOptions(opt.mapOptions);
  803. else $.extend(true, self.options, opt.mapOptions);
  804. // IF we update areas, plots or legend, then reset all legend state to "show"
  805. if (opt.mapOptions.areas !== undefined || opt.mapOptions.plots !== undefined || opt.mapOptions.legend !== undefined) {
  806. $("[data-type='elem']", self.$container).each(function (id, elem) {
  807. if ($(elem).attr('data-hidden') === "1") {
  808. // Toggle state of element by clicking
  809. $(elem).trigger("click." + pluginName, [false, animDuration]);
  810. }
  811. });
  812. }
  813. }
  814. // Delete plots by name if deletePlotKeys is array
  815. if (typeof opt.deletePlotKeys === "object") {
  816. for (; i < opt.deletePlotKeys.length; i++) {
  817. if (self.plots[opt.deletePlotKeys[i]] !== undefined) {
  818. fnRemoveElement(self.plots[opt.deletePlotKeys[i]]);
  819. delete self.plots[opt.deletePlotKeys[i]];
  820. }
  821. }
  822. // Delete ALL plots if deletePlotKeys is set to "all"
  823. } else if (opt.deletePlotKeys === "all") {
  824. $.each(self.plots, function (id, elem) {
  825. fnRemoveElement(elem);
  826. });
  827. // Empty plots object
  828. self.plots = {};
  829. }
  830. // Delete links by name if deleteLinkKeys is array
  831. if (typeof opt.deleteLinkKeys === "object") {
  832. for (i = 0; i < opt.deleteLinkKeys.length; i++) {
  833. if (self.links[opt.deleteLinkKeys[i]] !== undefined) {
  834. fnRemoveElement(self.links[opt.deleteLinkKeys[i]]);
  835. delete self.links[opt.deleteLinkKeys[i]];
  836. }
  837. }
  838. // Delete ALL links if deleteLinkKeys is set to "all"
  839. } else if (opt.deleteLinkKeys === "all") {
  840. $.each(self.links, function (id, elem) {
  841. fnRemoveElement(elem);
  842. });
  843. // Empty links object
  844. self.links = {};
  845. }
  846. // New plots
  847. if (typeof opt.newPlots === "object") {
  848. $.each(opt.newPlots, function (id) {
  849. if (self.plots[id] === undefined) {
  850. self.options.plots[id] = opt.newPlots[id];
  851. self.plots[id] = self.drawPlot(id);
  852. if (animDuration > 0) {
  853. fnShowElement(self.plots[id]);
  854. }
  855. }
  856. });
  857. }
  858. // New links
  859. if (typeof opt.newLinks === "object") {
  860. var newLinks = self.drawLinksCollection(opt.newLinks);
  861. $.extend(self.links, newLinks);
  862. $.extend(self.options.links, opt.newLinks);
  863. if (animDuration > 0) {
  864. $.each(newLinks, function (id) {
  865. fnShowElement(newLinks[id]);
  866. });
  867. }
  868. }
  869. // Update areas attributes and tooltips
  870. $.each(self.areas, function (id) {
  871. // Avoid updating unchanged elements
  872. if ((typeof opt.mapOptions === "object" &&
  873. (
  874. (typeof opt.mapOptions.map === "object" && typeof opt.mapOptions.map.defaultArea === "object")
  875. || (typeof opt.mapOptions.areas === "object" && typeof opt.mapOptions.areas[id] === "object")
  876. || (typeof opt.mapOptions.legend === "object" && typeof opt.mapOptions.legend.area === "object")
  877. ))
  878. || opt.replaceOptions === true
  879. ) {
  880. var elemOptions = self.getElemOptions(
  881. self.options.map.defaultArea,
  882. (self.options.areas[id] ? self.options.areas[id] : {}),
  883. self.options.legend.area
  884. );
  885. self.updateElem(elemOptions, self.areas[id], animDuration);
  886. }
  887. });
  888. // Update plots attributes and tooltips
  889. $.each(self.plots, function (id) {
  890. // Avoid updating unchanged elements
  891. if ((typeof opt.mapOptions ==="object" &&
  892. (
  893. (typeof opt.mapOptions.map === "object" && typeof opt.mapOptions.map.defaultPlot === "object")
  894. || (typeof opt.mapOptions.plots === "object" && typeof opt.mapOptions.plots[id] === "object")
  895. || (typeof opt.mapOptions.legend === "object" && typeof opt.mapOptions.legend.plot === "object")
  896. ))
  897. || opt.replaceOptions === true
  898. ) {
  899. var elemOptions = self.getElemOptions(
  900. self.options.map.defaultPlot,
  901. (self.options.plots[id] ? self.options.plots[id] : {}),
  902. self.options.legend.plot
  903. );
  904. if (elemOptions.type == "square") {
  905. elemOptions.attrs.width = elemOptions.size;
  906. elemOptions.attrs.height = elemOptions.size;
  907. elemOptions.attrs.x = self.plots[id].mapElem.attrs.x - (elemOptions.size - self.plots[id].mapElem.attrs.width) / 2;
  908. elemOptions.attrs.y = self.plots[id].mapElem.attrs.y - (elemOptions.size - self.plots[id].mapElem.attrs.height) / 2;
  909. } else if (elemOptions.type == "image") {
  910. elemOptions.attrs.width = elemOptions.width;
  911. elemOptions.attrs.height = elemOptions.height;
  912. elemOptions.attrs.x = self.plots[id].mapElem.attrs.x - (elemOptions.width - self.plots[id].mapElem.attrs.width) / 2;
  913. elemOptions.attrs.y = self.plots[id].mapElem.attrs.y - (elemOptions.height - self.plots[id].mapElem.attrs.height) / 2;
  914. } else if (elemOptions.type == "svg") {
  915. if (elemOptions.attrs.transform !== undefined) {
  916. elemOptions.attrs.transform = self.plots[id].mapElem.baseTransform + elemOptions.attrs.transform;
  917. }
  918. }else { // Default : circle
  919. elemOptions.attrs.r = elemOptions.size / 2;
  920. }
  921. self.updateElem(elemOptions, self.plots[id], animDuration);
  922. }
  923. });
  924. // Update links attributes and tooltips
  925. $.each(self.links, function (id) {
  926. // Avoid updating unchanged elements
  927. if ((typeof opt.mapOptions === "object" &&
  928. (
  929. (typeof opt.mapOptions.map === "object" && typeof opt.mapOptions.map.defaultLink === "object")
  930. || (typeof opt.mapOptions.links === "object" && typeof opt.mapOptions.links[id] === "object")
  931. ))
  932. || opt.replaceOptions === true
  933. ) {
  934. var elemOptions = self.getElemOptions(
  935. self.options.map.defaultLink,
  936. (self.options.links[id] ? self.options.links[id] : {}),
  937. {}
  938. );
  939. self.updateElem(elemOptions, self.links[id], animDuration);
  940. }
  941. });
  942. // Update legends
  943. if (opt.mapOptions && (
  944. (typeof opt.mapOptions.legend === "object")
  945. || (typeof opt.mapOptions.map === "object" && typeof opt.mapOptions.map.defaultArea === "object")
  946. || (typeof opt.mapOptions.map === "object" && typeof opt.mapOptions.map.defaultPlot === "object")
  947. )) {
  948. // Show all elements on the map before updating the legends
  949. $("[data-type='elem']", self.$container).each(function (id, elem) {
  950. if ($(elem).attr('data-hidden') === "1") {
  951. $(elem).trigger("click." + pluginName, [false, animDuration]);
  952. }
  953. });
  954. self.createLegends("area", self.areas, 1);
  955. if (self.options.map.width) {
  956. self.createLegends("plot", self.plots, (self.options.map.width / self.mapConf.width));
  957. } else {
  958. self.createLegends("plot", self.plots, (self.$map.width() / self.mapConf.width));
  959. }
  960. }
  961. // Hide/Show all elements based on showlegendElems
  962. // Toggle (i.e. click) only if:
  963. // - slice legend is shown AND we want to hide
  964. // - slice legend is hidden AND we want to show
  965. if (typeof opt.setLegendElemsState === "object") {
  966. // setLegendElemsState is an object listing the legend we want to hide/show
  967. $.each(opt.setLegendElemsState, function (legendCSSClass, action) {
  968. // Search for the legend
  969. var $legend = self.$container.find("." + legendCSSClass)[0];
  970. if ($legend !== undefined) {
  971. // Select all elem inside this legend
  972. $("[data-type='elem']", $legend).each(function (id, elem) {
  973. if (($(elem).attr('data-hidden') === "0" && action === "hide") ||
  974. ($(elem).attr('data-hidden') === "1" && action === "show")) {
  975. // Toggle state of element by clicking
  976. $(elem).trigger("click." + pluginName, [false, animDuration]);
  977. }
  978. });
  979. }
  980. });
  981. } else {
  982. // setLegendElemsState is a string, or is undefined
  983. // Default : "show"
  984. var action = (opt.setLegendElemsState === "hide") ? "hide" : "show";
  985. $("[data-type='elem']", self.$container).each(function (id, elem) {
  986. if (($(elem).attr('data-hidden') === "0" && action === "hide") ||
  987. ($(elem).attr('data-hidden') === "1" && action === "show")) {
  988. // Toggle state of element by clicking
  989. $(elem).trigger("click." + pluginName, [false, animDuration]);
  990. }
  991. });
  992. }
  993. if (opt.afterUpdate) opt.afterUpdate(self.$container, self.paper, self.areas, self.plots, self.options);
  994. },
  995. /*
  996. * Draw all links between plots on the paper
  997. */
  998. drawLinksCollection: function (linksCollection) {
  999. var self = this;
  1000. var p1 = {};
  1001. var p2 = {};
  1002. var coordsP1 = {};
  1003. var coordsP2 = {};
  1004. var links = {};
  1005. $.each(linksCollection, function (id) {
  1006. var elemOptions = self.getElemOptions(self.options.map.defaultLink, linksCollection[id], {});
  1007. if (typeof linksCollection[id].between[0] == 'string') {
  1008. p1 = self.options.plots[linksCollection[id].between[0]];
  1009. } else {
  1010. p1 = linksCollection[id].between[0];
  1011. }
  1012. if (typeof linksCollection[id].between[1] == 'string') {
  1013. p2 = self.options.plots[linksCollection[id].between[1]];
  1014. } else {
  1015. p2 = linksCollection[id].between[1];
  1016. }
  1017. if (p1.latitude !== undefined && p1.longitude !== undefined) {
  1018. coordsP1 = self.mapConf.getCoords(p1.latitude, p1.longitude);
  1019. } else {
  1020. coordsP1.x = p1.x;
  1021. coordsP1.y = p1.y;
  1022. }
  1023. if (p2.latitude !== undefined && p2.longitude !== undefined) {
  1024. coordsP2 = self.mapConf.getCoords(p2.latitude, p2.longitude);
  1025. } else {
  1026. coordsP2.x = p2.x;
  1027. coordsP2.y = p2.y;
  1028. }
  1029. links[id] = self.drawLink(id, coordsP1.x, coordsP1.y, coordsP2.x, coordsP2.y, elemOptions);
  1030. });
  1031. return links;
  1032. },
  1033. /*
  1034. * Draw a curved link between two couples of coordinates a(xa,ya) and b(xb, yb) on the paper
  1035. */
  1036. drawLink: function (id, xa, ya, xb, yb, elemOptions) {
  1037. var self = this;
  1038. var elem = {};
  1039. // Compute the "curveto" SVG point, d(x,y)
  1040. // c(xc, yc) is the center of (xa,ya) and (xb, yb)
  1041. var xc = (xa + xb) / 2;
  1042. var yc = (ya + yb) / 2;
  1043. // Equation for (cd) : y = acd * x + bcd (d is the cure point)
  1044. var acd = -1 / ((yb - ya) / (xb - xa));
  1045. var bcd = yc - acd * xc;
  1046. // dist(c,d) = dist(a,b) (=abDist)
  1047. var abDist = Math.sqrt((xb - xa) * (xb - xa) + (yb - ya) * (yb - ya));
  1048. // Solution for equation dist(cd) = sqrt((xd - xc)² + (yd - yc)²)
  1049. // dist(c,d)² = (xd - xc)² + (yd - yc)²
  1050. // We assume that dist(c,d) = dist(a,b)
  1051. // so : (xd - xc)² + (yd - yc)² - dist(a,b)² = 0
  1052. // With the factor : (xd - xc)² + (yd - yc)² - (factor*dist(a,b))² = 0
  1053. // (xd - xc)² + (acd*xd + bcd - yc)² - (factor*dist(a,b))² = 0
  1054. var a = 1 + acd * acd;
  1055. var b = -2 * xc + 2 * acd * bcd - 2 * acd * yc;
  1056. var c = xc * xc + bcd * bcd - bcd * yc - yc * bcd + yc * yc - ((elemOptions.factor * abDist) * (elemOptions.factor * abDist));
  1057. var delta = b * b - 4 * a * c;
  1058. var x = 0;
  1059. var y = 0;
  1060. // There are two solutions, we choose one or the other depending on the sign of the factor
  1061. if (elemOptions.factor > 0) {
  1062. x = (-b + Math.sqrt(delta)) / (2 * a);
  1063. y = acd * x + bcd;
  1064. } else {
  1065. x = (-b - Math.sqrt(delta)) / (2 * a);
  1066. y = acd * x + bcd;
  1067. }
  1068. elem.mapElem = self.paper.path("m " + xa + "," + ya + " C " + x + "," + y + " " + xb + "," + yb + " " + xb + "," + yb + "").attr(elemOptions.attrs);
  1069. self.initElem(elem, elemOptions, id);
  1070. return elem;
  1071. },
  1072. /*
  1073. * Update the element "elem" on the map with the new elemOptions options
  1074. */
  1075. updateElem: function (elemOptions, elem, animDuration) {
  1076. var self = this;
  1077. var bbox = elem.mapElem.getBBox();
  1078. var textPosition;
  1079. var plotOffsetX;
  1080. var plotOffsetY;
  1081. if (elemOptions.value !== undefined)
  1082. elem.value = elemOptions.value;
  1083. // Update the label
  1084. if (elem.textElem) {
  1085. if (elemOptions.text !== undefined && elemOptions.text.content !== undefined && elemOptions.text.content != elem.textElem.attrs.text)
  1086. elem.textElem.attr({text: elemOptions.text.content});
  1087. if (elemOptions.size || (elemOptions.width && elemOptions.height)) {
  1088. if (elemOptions.type == "image" || elemOptions.type == "svg") {
  1089. plotOffsetX = (elemOptions.width - bbox.width) / 2;
  1090. plotOffsetY = (elemOptions.height - bbox.height) / 2;
  1091. } else {
  1092. plotOffsetX = (elemOptions.size - bbox.width) / 2;
  1093. plotOffsetY = (elemOptions.size - bbox.height) / 2;
  1094. }
  1095. bbox.x -= plotOffsetX;
  1096. bbox.x2 += plotOffsetX;
  1097. bbox.y -= plotOffsetY;
  1098. bbox.y2 += plotOffsetY;
  1099. }
  1100. textPosition = self.getTextPosition(bbox, elemOptions.text.position, elemOptions.text.margin);
  1101. if (textPosition.x != elem.textElem.attrs.x || textPosition.y != elem.textElem.attrs.y) {
  1102. if (animDuration > 0) {
  1103. elem.textElem.attr({"text-anchor": textPosition.textAnchor});
  1104. elem.textElem.animate({x: textPosition.x, y: textPosition.y}, animDuration);
  1105. } else
  1106. elem.textElem.attr({
  1107. x: textPosition.x,
  1108. y: textPosition.y,
  1109. "text-anchor": textPosition.textAnchor
  1110. });
  1111. }
  1112. self.setHoverOptions(elem.textElem, elemOptions.text.attrs, elemOptions.text.attrsHover);
  1113. if (animDuration > 0)
  1114. elem.textElem.animate(elemOptions.text.attrs, animDuration);
  1115. else
  1116. elem.textElem.attr(elemOptions.text.attrs);
  1117. }
  1118. // Update elements attrs and attrsHover
  1119. self.setHoverOptions(elem.mapElem, elemOptions.attrs, elemOptions.attrsHover);
  1120. if (animDuration > 0)
  1121. elem.mapElem.animate(elemOptions.attrs, animDuration);
  1122. else
  1123. elem.mapElem.attr(elemOptions.attrs);
  1124. // Update dimensions of SVG plots
  1125. if (elemOptions.type == "svg") {
  1126. elem.mapElem.transform("m" + (elemOptions.width / elem.mapElem.originalWidth) + ",0,0," + (elemOptions.height / elem.mapElem.originalHeight) + "," + bbox.x + "," + bbox.y);
  1127. }
  1128. // Update the tooltip
  1129. if (elemOptions.tooltip) {
  1130. if (elem.mapElem.tooltip === undefined) {
  1131. self.setTooltip(elem.mapElem);
  1132. if (elem.textElem) self.setTooltip(elem.textElem);
  1133. }
  1134. elem.mapElem.tooltip = elemOptions.tooltip;
  1135. if (elem.textElem) elem.textElem.tooltip = elemOptions.tooltip;
  1136. }
  1137. // Update the link
  1138. if (elemOptions.href !== undefined) {
  1139. if (elem.mapElem.href === undefined) {
  1140. self.setHref(elem.mapElem);
  1141. if (elem.textElem) self.setHref(elem.textElem);
  1142. }
  1143. elem.mapElem.href = elemOptions.href;
  1144. elem.mapElem.target = elemOptions.target;
  1145. if (elem.textElem) {
  1146. elem.textElem.href = elemOptions.href;
  1147. elem.textElem.target = elemOptions.target;
  1148. }
  1149. }
  1150. },
  1151. /*
  1152. * Draw the plot
  1153. */
  1154. drawPlot: function (id) {
  1155. var self = this;
  1156. var plot = {};
  1157. var coords = {};
  1158. var elemOptions = self.getElemOptions(
  1159. self.options.map.defaultPlot,
  1160. (self.options.plots[id] ? self.options.plots[id] : {}),
  1161. self.options.legend.plot
  1162. );
  1163. if (elemOptions.x !== undefined && elemOptions.y !== undefined)
  1164. coords = {x: elemOptions.x, y: elemOptions.y};
  1165. else
  1166. coords = self.mapConf.getCoords(elemOptions.latitude, elemOptions.longitude);
  1167. if (elemOptions.type == "square") {
  1168. plot = {
  1169. "mapElem": self.paper.rect(
  1170. coords.x - (elemOptions.size / 2),
  1171. coords.y - (elemOptions.size / 2),
  1172. elemOptions.size,
  1173. elemOptions.size
  1174. ).attr(elemOptions.attrs)
  1175. };
  1176. } else if (elemOptions.type == "image") {
  1177. plot = {
  1178. "mapElem": self.paper.image(
  1179. elemOptions.url,
  1180. coords.x - elemOptions.width / 2,
  1181. coords.y - elemOptions.height / 2,
  1182. elemOptions.width,
  1183. elemOptions.height
  1184. ).attr(elemOptions.attrs)
  1185. };
  1186. } else if (elemOptions.type == "svg") {
  1187. if (elemOptions.attrs.transform === undefined) {
  1188. elemOptions.attrs.transform = "";
  1189. }
  1190. plot = {"mapElem": self.paper.path(elemOptions.path)};
  1191. plot.mapElem.originalWidth = plot.mapElem.getBBox().width;
  1192. plot.mapElem.originalHeight = plot.mapElem.getBBox().height;
  1193. plot.mapElem.baseTransform = "m" + (elemOptions.width / plot.mapElem.originalWidth) + ",0,0," + (elemOptions.height / plot.mapElem.originalHeight) + "," + (coords.x - elemOptions.width / 2) + "," + (coords.y - elemOptions.height / 2);
  1194. elemOptions.attrs.transform = plot.mapElem.baseTransform + elemOptions.attrs.transform;
  1195. plot.mapElem.attr(elemOptions.attrs);
  1196. } else { // Default = circle
  1197. plot = {"mapElem": self.paper.circle(coords.x, coords.y, elemOptions.size / 2).attr(elemOptions.attrs)};
  1198. }
  1199. self.initElem(plot, elemOptions, id);
  1200. return plot;
  1201. },
  1202. /*
  1203. * Set target link on elem
  1204. */
  1205. setHref: function (elem) {
  1206. var self = this;
  1207. elem.attr({cursor: "pointer"});
  1208. $(elem.node).on("click." + pluginName, function () {
  1209. if (!self.panning && elem.href)
  1210. window.open(elem.href, elem.target);
  1211. });
  1212. },
  1213. /*
  1214. * Set a tooltip for the areas and plots
  1215. * @param elem area or plot element
  1216. * @param content the content to set in the tooltip
  1217. */
  1218. setTooltip: function (elem) {
  1219. var self = this;
  1220. var tooltipTO = 0;
  1221. var cssClass = self.$tooltip.attr('class');
  1222. var updateTooltipPosition = function (x, y) {
  1223. var offsetLeft = 10;
  1224. var offsetTop = 20;
  1225. if (typeof elem.tooltip.offset === "object") {
  1226. if (typeof elem.tooltip.offset.left !== "undefined") {
  1227. offsetLeft = elem.tooltip.offset.left;
  1228. }
  1229. if (typeof elem.tooltip.offset.top !== "undefined") {
  1230. offsetTop = elem.tooltip.offset.top;
  1231. }
  1232. }
  1233. var tooltipPosition = {
  1234. "left": Math.min(self.$map.width() - self.$tooltip.outerWidth() - 5, x - self.$map.offset().left + offsetLeft),
  1235. "top": Math.min(self.$map.height() - self.$tooltip.outerHeight() - 5, y - self.$map.offset().top + offsetTop)
  1236. };
  1237. if (typeof elem.tooltip.overflow === "object") {
  1238. if (elem.tooltip.overflow.right === true) {
  1239. tooltipPosition.left = x - self.$map.offset().left + 10;
  1240. }
  1241. if (selem.tooltip.overflow.bottom === true) {
  1242. tooltipPosition.top = y - self.$map.offset().top + 20;
  1243. }
  1244. }
  1245. self.$tooltip.css(tooltipPosition);
  1246. };
  1247. $(elem.node).on("mouseover." + pluginName, function (e) {
  1248. tooltipTO = setTimeout(
  1249. function () {
  1250. self.$tooltip.attr("class", cssClass);
  1251. if (elem.tooltip !== undefined) {
  1252. if (elem.tooltip.content !== undefined) {
  1253. // if tooltip.content is function, call it. Otherwise, assign it directly.
  1254. var content = (typeof elem.tooltip.content === "function") ? elem.tooltip.content(elem) : elem.tooltip.content;
  1255. self.$tooltip.html(content).css("display", "block");
  1256. }
  1257. if (elem.tooltip.cssClass !== undefined) {
  1258. self.$tooltip.addClass(elem.tooltip.cssClass);
  1259. }
  1260. }
  1261. updateTooltipPosition(e.pageX, e.pageY);
  1262. }, 120
  1263. );
  1264. }).on("mouseout." + pluginName, function () {
  1265. clearTimeout(tooltipTO);
  1266. self.$tooltip.css("display", "none");
  1267. }).on("mousemove." + pluginName, function (e) {
  1268. updateTooltipPosition(e.pageX, e.pageY);
  1269. });
  1270. },
  1271. /*
  1272. * Set user defined handlers for events on areas and plots
  1273. * @param id the id of the element
  1274. * @param elemOptions the element parameters
  1275. * @param mapElem the map element to set callback on
  1276. * @param textElem the optional text within the map element
  1277. */
  1278. setEventHandlers: function (id, elemOptions, mapElem, textElem) {
  1279. var self = this;
  1280. $.each(elemOptions.eventHandlers, function (event) {
  1281. (function (event) {
  1282. $(mapElem.node).on(event, function (e) {
  1283. if (!self.panning) elemOptions.eventHandlers[event](e, id, mapElem, textElem, elemOptions);
  1284. });
  1285. if (textElem) {
  1286. $(textElem.node).on(event, function (e) {
  1287. if (!self.panning) elemOptions.eventHandlers[event](e, id, mapElem, textElem, elemOptions);
  1288. });
  1289. }
  1290. })(event);
  1291. });
  1292. },
  1293. /*
  1294. * Draw a legend for areas and / or plots
  1295. * @param legendOptions options for the legend to draw
  1296. * @param legendType the type of the legend : "area" or "plot"
  1297. * @param elems collection of plots or areas on the maps
  1298. * @param legendIndex index of the legend in the conf array
  1299. */
  1300. drawLegend: function (legendOptions, legendType, elems, scale, legendIndex) {
  1301. var self = this;
  1302. var $legend = {};
  1303. var legendPaper = {};
  1304. var width = 0;
  1305. var height = 0;
  1306. var title = null;
  1307. var elem = {};
  1308. var elemBBox = {};
  1309. var label = {};
  1310. var i = 0;
  1311. var x = 0;
  1312. var y = 0;
  1313. var yCenter = 0;
  1314. var sliceOptions = [];
  1315. var length = 0;
  1316. $legend = $("." + legendOptions.cssClass, self.$container).empty();
  1317. legendPaper = new Raphael($legend.get(0));
  1318. // Set some data to object
  1319. $(legendPaper.canvas).attr({"data-type": legendType, "data-index": legendIndex});
  1320. height = width = 0;
  1321. // Set the title of the legend
  1322. if (legendOptions.title && legendOptions.title !== "") {
  1323. title = legendPaper.text(legendOptions.marginLeftTitle, 0, legendOptions.title).attr(legendOptions.titleAttrs);
  1324. title.attr({y: 0.5 * title.getBBox().height});
  1325. width = legendOptions.marginLeftTitle + title.getBBox().width;
  1326. height += legendOptions.marginBottomTitle + title.getBBox().height;
  1327. }
  1328. // Calculate attrs (and width, height and r (radius)) for legend elements, and yCenter for horizontal legends
  1329. for (i = 0, length = legendOptions.slices.length; i < length; ++i) {
  1330. var yCenterCurrent = 0;
  1331. sliceOptions[i] = $.extend(true, {}, (legendType == "plot") ? self.options.map.defaultPlot : self.options.map.defaultArea, legendOptions.slices[i]);
  1332. if (legendOptions.slices[i].legendSpecificAttrs === undefined) {
  1333. legendOptions.slices[i].legendSpecificAttrs = {};
  1334. }
  1335. $.extend(true, sliceOptions[i].attrs, legendOptions.slices[i].legendSpecificAttrs);
  1336. if (legendType == "area") {
  1337. if (sliceOptions[i].attrs.width === undefined)
  1338. sliceOptions[i].attrs.width = 30;
  1339. if (sliceOptions[i].attrs.height === undefined)
  1340. sliceOptions[i].attrs.height = 20;
  1341. } else if (sliceOptions[i].type == "square") {
  1342. if (sliceOptions[i].attrs.width === undefined)
  1343. sliceOptions[i].attrs.width = sliceOptions[i].size;
  1344. if (sliceOptions[i].attrs.height === undefined)
  1345. sliceOptions[i].attrs.height = sliceOptions[i].size;
  1346. } else if (sliceOptions[i].type == "image" || sliceOptions[i].type == "svg") {
  1347. if (sliceOptions[i].attrs.width === undefined)
  1348. sliceOptions[i].attrs.width = sliceOptions[i].width;
  1349. if (sliceOptions[i].attrs.height === undefined)
  1350. sliceOptions[i].attrs.height = sliceOptions[i].height;
  1351. } else {
  1352. if (sliceOptions[i].attrs.r === undefined)
  1353. sliceOptions[i].attrs.r = sliceOptions[i].size / 2;
  1354. }
  1355. // Compute yCenter for this legend slice
  1356. yCenterCurrent = legendOptions.marginBottomTitle;
  1357. // Add title height if it exists
  1358. if (title) {
  1359. yCenterCurrent += title.getBBox().height;
  1360. }
  1361. if (legendType == "plot" && (sliceOptions[i].type === undefined || sliceOptions[i].type == "circle")) {
  1362. yCenterCurrent += scale * sliceOptions[i].attrs.r;
  1363. } else {
  1364. yCenterCurrent += scale * sliceOptions[i].attrs.height / 2;
  1365. }
  1366. // Update yCenter if current larger
  1367. yCenter = Math.max(yCenter, yCenterCurrent);
  1368. }
  1369. if (legendOptions.mode == "horizontal") {
  1370. width = legendOptions.marginLeft;
  1371. }
  1372. // Draw legend elements (circle, square or image in vertical or horizontal mode)
  1373. for (i = 0, length = sliceOptions.length; i < length; ++i) {
  1374. if (sliceOptions[i].display === undefined || sliceOptions[i].display === true) {
  1375. if (legendType == "area") {
  1376. if (legendOptions.mode == "horizontal") {
  1377. x = width + legendOptions.marginLeft;
  1378. y = yCenter - (0.5 * scale * sliceOptions[i].attrs.height);
  1379. } else {
  1380. x = legendOptions.marginLeft;
  1381. y = height;
  1382. }
  1383. elem = legendPaper.rect(x, y, scale * (sliceOptions[i].attrs.width), scale * (sliceOptions[i].attrs.height));
  1384. } else if (sliceOptions[i].type == "square") {
  1385. if (legendOptions.mode == "horizontal") {
  1386. x = width + legendOptions.marginLeft;
  1387. y = yCenter - (0.5 * scale * sliceOptions[i].attrs.height);
  1388. } else {
  1389. x = legendOptions.marginLeft;
  1390. y = height;
  1391. }
  1392. elem = legendPaper.rect(x, y, scale * (sliceOptions[i].attrs.width), scale * (sliceOptions[i].attrs.height));
  1393. } else if (sliceOptions[i].type == "image" || sliceOptions[i].type == "svg") {
  1394. if (legendOptions.mode == "horizontal") {
  1395. x = width + legendOptions.marginLeft;
  1396. y = yCenter - (0.5 * scale * sliceOptions[i].attrs.height);
  1397. } else {
  1398. x = legendOptions.marginLeft;
  1399. y = height;
  1400. }
  1401. if (sliceOptions[i].type == "image") {
  1402. elem = legendPaper.image(
  1403. sliceOptions[i].url, x, y, scale * sliceOptions[i].attrs.width, scale * sliceOptions[i].attrs.height);
  1404. } else {
  1405. elem = legendPaper.path(sliceOptions[i].path);
  1406. if (sliceOptions[i].attrs.transform === undefined) {
  1407. sliceOptions[i].attrs.transform = "";
  1408. }
  1409. sliceOptions[i].attrs.transform = "m" + ((scale * sliceOptions[i].width) / elem.getBBox().width) + ",0,0," + ((scale * sliceOptions[i].height) / elem.getBBox().height) + "," + x + "," + y + sliceOptions[i].attrs.transform;
  1410. }
  1411. } else {
  1412. if (legendOptions.mode == "horizontal") {
  1413. x = width + legendOptions.marginLeft + scale * (sliceOptions[i].attrs.r);
  1414. y = yCenter;
  1415. } else {
  1416. x = legendOptions.marginLeft + scale * (sliceOptions[i].attrs.r);
  1417. y = height + scale * (sliceOptions[i].attrs.r);
  1418. }
  1419. elem = legendPaper.circle(x, y, scale * (sliceOptions[i].attrs.r));
  1420. }
  1421. // Set attrs to the element drawn above
  1422. delete sliceOptions[i].attrs.width;
  1423. delete sliceOptions[i].attrs.height;
  1424. delete sliceOptions[i].attrs.r;
  1425. elem.attr(sliceOptions[i].attrs);
  1426. elemBBox = elem.getBBox();
  1427. // Draw the label associated with the element
  1428. if (legendOptions.mode == "horizontal") {
  1429. x = width + legendOptions.marginLeft + elemBBox.width + legendOptions.marginLeftLabel;
  1430. y = yCenter;
  1431. } else {
  1432. x = legendOptions.marginLeft + elemBBox.width + legendOptions.marginLeftLabel;
  1433. y = height + (elemBBox.height / 2);
  1434. }
  1435. label = legendPaper.text(x, y, sliceOptions[i].label).attr(legendOptions.labelAttrs);
  1436. // Update the width and height for the paper
  1437. if (legendOptions.mode == "horizontal") {
  1438. var currentHeight = legendOptions.marginBottom + elemBBox.height;
  1439. width += legendOptions.marginLeft + elemBBox.width + legendOptions.marginLeftLabel + label.getBBox().width;
  1440. if (sliceOptions[i].type != "image" && legendType != "area") {
  1441. currentHeight += legendOptions.marginBottomTitle;
  1442. }
  1443. // Add title height if it exists
  1444. if (title) {
  1445. currentHeight += title.getBBox().height;
  1446. }
  1447. height = Math.max(height, currentHeight);
  1448. } else {
  1449. width = Math.max(width, legendOptions.marginLeft + elemBBox.width + legendOptions.marginLeftLabel + label.getBBox().width);
  1450. height += legendOptions.marginBottom + elemBBox.height;
  1451. }
  1452. $(elem.node).attr({"data-type": "elem", "data-index": i, "data-hidden": 0});
  1453. $(label.node).attr({"data-type": "label", "data-index": i, "data-hidden": 0});
  1454. // Hide map elements when the user clicks on a legend item
  1455. if (legendOptions.hideElemsOnClick.enabled) {
  1456. // Hide/show elements when user clicks on a legend element
  1457. label.attr({cursor: "pointer"});
  1458. elem.attr({cursor: "pointer"});
  1459. self.setHoverOptions(elem, sliceOptions[i].attrs, sliceOptions[i].attrs);
  1460. self.setHoverOptions(label, legendOptions.labelAttrs, legendOptions.labelAttrsHover);
  1461. self.setHover(elem, label);
  1462. self.handleClickOnLegendElem(legendOptions, sliceOptions[i], label, elem, elems, legendIndex);
  1463. }
  1464. }
  1465. }
  1466. // VMLWidth option allows you to set static width for the legend
  1467. // only for VML render because text.getBBox() returns wrong values on IE6/7
  1468. if (Raphael.type != "SVG" && legendOptions.VMLWidth)
  1469. width = legendOptions.VMLWidth;
  1470. legendPaper.setSize(width, height);
  1471. },
  1472. /*
  1473. * Allow to hide elements of the map when the user clicks on a related legend item
  1474. * @param legendOptions options for the legend to draw
  1475. * @param sliceOptions options of the slice
  1476. * @param label label of the legend item
  1477. * @param elem element of the legend item
  1478. * @param elems collection of plots or areas displayed on the map
  1479. * @param legendIndex index of the legend in the conf array
  1480. */
  1481. handleClickOnLegendElem: function (legendOptions, sliceOptions, label, elem, elems, legendIndex) {
  1482. var self = this;
  1483. var hideMapElems = function (e, hideOtherElems, animDuration) {
  1484. var elemValue = 0;
  1485. var hidden = $(label.node).attr('data-hidden');
  1486. var hiddenNewAttr = (hidden === '0') ? {"data-hidden": '1'} : {"data-hidden": '0'};
  1487. // Check animDuration: if not set, this is a regular click, use the value specified in options
  1488. if (animDuration === undefined) animDuration = legendOptions.hideElemsOnClick.animDuration;
  1489. if (hidden === '0') {
  1490. if (animDuration > 0) label.animate({"opacity": 0.5}, animDuration);
  1491. else label.attr({"opacity": 0.5});
  1492. } else {
  1493. if (animDuration > 0) label.animate({"opacity": 1}, animDuration);
  1494. else label.attr({"opacity": 1});
  1495. }
  1496. $.each(elems, function (id) {
  1497. // Retreive stored data of element
  1498. // 'hidden-by' contains the list of legendIndex that is hiding this element
  1499. var hiddenBy = elems[id].mapElem.data('hidden-by');
  1500. // Set to empty object if undefined
  1501. if (hiddenBy === undefined) hiddenBy = {};
  1502. if ($.isArray(elems[id].value)) {
  1503. elemValue = elems[id].value[legendIndex];
  1504. } else {
  1505. elemValue = elems[id].value;
  1506. }
  1507. if ((sliceOptions.sliceValue !== undefined && elemValue == sliceOptions.sliceValue)
  1508. || ((sliceOptions.sliceValue === undefined)
  1509. && (sliceOptions.min === undefined || elemValue >= sliceOptions.min)
  1510. && (sliceOptions.max === undefined || elemValue <= sliceOptions.max))
  1511. ) {
  1512. (function (id) {
  1513. if (hidden === '0') { // we want to hide this element
  1514. hiddenBy[legendIndex] = true; // add legendIndex to the data object for later use
  1515. self.setElementOpacity(elems[id], legendOptions.hideElemsOnClick.opacity, animDuration);
  1516. } else { // We want to show this element
  1517. delete hiddenBy[legendIndex]; // Remove this legendIndex from object
  1518. // Check if another legendIndex is defined
  1519. // We will show this element only if no legend is no longer hiding it
  1520. if ($.isEmptyObject(hiddenBy)) {
  1521. self.setElementOpacity(
  1522. elems[id],
  1523. elems[id].mapElem.originalAttrs.opacity !== undefined ? elems[id].mapElem.originalAttrs.opacity : 1,
  1524. animDuration
  1525. );
  1526. }
  1527. }
  1528. // Update elem data with new values
  1529. elems[id].mapElem.data('hidden-by', hiddenBy);
  1530. })(id);
  1531. }
  1532. });
  1533. $(elem.node).attr(hiddenNewAttr);
  1534. $(label.node).attr(hiddenNewAttr);
  1535. if ((hideOtherElems === undefined || hideOtherElems === true)
  1536. && legendOptions.exclusive !== undefined && legendOptions.exclusive === true
  1537. ) {
  1538. $("[data-type='elem'][data-hidden=0]", self.$container).each(function () {
  1539. if ($(this).attr('data-index') !== $(elem.node).attr('data-index')) {
  1540. $(this).trigger("click." + pluginName, false);
  1541. }
  1542. });
  1543. }
  1544. };
  1545. $(label.node).on("click." + pluginName, hideMapElems);
  1546. $(elem.node).on("click." + pluginName, hideMapElems);
  1547. if (sliceOptions.clicked !== undefined && sliceOptions.clicked === true) {
  1548. $(elem.node).trigger("click." + pluginName, false);
  1549. }
  1550. },
  1551. /*
  1552. * Create all legends for a specified type (area or plot)
  1553. * @param legendType the type of the legend : "area" or "plot"
  1554. * @param elems collection of plots or areas displayed on the map
  1555. * @param scale scale ratio of the map
  1556. */
  1557. createLegends: function (legendType, elems, scale) {
  1558. var self = this;
  1559. var legendsOptions = self.options.legend[legendType];
  1560. if (!$.isArray(self.options.legend[legendType])) {
  1561. legendsOptions = [self.options.legend[legendType]];
  1562. }
  1563. for (var j = 0; j < legendsOptions.length; ++j) {
  1564. // Check for class existence
  1565. if (legendsOptions[j].cssClass === "" || $("." + legendsOptions[j].cssClass, self.$container).length === 0) {
  1566. throw new Error("The legend class `" + legendsOptions[j].cssClass + "` doesn't exists.");
  1567. }
  1568. if (legendsOptions[j].display === true && $.isArray(legendsOptions[j].slices) && legendsOptions[j].slices.length > 0) {
  1569. self.drawLegend(legendsOptions[j], legendType, elems, scale, j);
  1570. }
  1571. }
  1572. },
  1573. /*
  1574. * Set the attributes on hover and the attributes to restore for a map element
  1575. * @param elem the map element
  1576. * @param originalAttrs the original attributes to restore on mouseout event
  1577. * @param attrsHover the attributes to set on mouseover event
  1578. */
  1579. setHoverOptions: function (elem, originalAttrs, attrsHover) {
  1580. // Disable transform option on hover for VML (IE<9) because of several bugs
  1581. if (Raphael.type != "SVG") delete attrsHover.transform;
  1582. elem.attrsHover = attrsHover;
  1583. if (elem.attrsHover.transform) elem.originalAttrs = $.extend({transform: "s1"}, originalAttrs);
  1584. else elem.originalAttrs = originalAttrs;
  1585. },
  1586. /*
  1587. * Set the hover behavior (mouseover & mouseout) for plots and areas
  1588. * @param mapElem the map element
  1589. * @param textElem the optional text element (within the map element)
  1590. */
  1591. setHover: function (mapElem, textElem) {
  1592. var self = this;
  1593. var $mapElem = {};
  1594. var $textElem = {};
  1595. var mouseoverTimeout = 0;
  1596. var mouseoutTimeout = 0;
  1597. var overBehaviour = function () {
  1598. clearTimeout(mouseoutTimeout);
  1599. mouseoverTimeout = setTimeout(function () {
  1600. self.elemHover(mapElem, textElem);
  1601. }, 120);
  1602. };
  1603. var outBehaviour = function () {
  1604. clearTimeout(mouseoverTimeout);
  1605. mouseoutTimeout = setTimeout(function(){
  1606. self.elemOut(mapElem, textElem);
  1607. }, 120);
  1608. };
  1609. $mapElem = $(mapElem.node);
  1610. $mapElem.on("mouseover." + pluginName, overBehaviour);
  1611. $mapElem.on("mouseout." + pluginName, outBehaviour);
  1612. if (textElem) {
  1613. $textElem = $(textElem.node);
  1614. $textElem.on("mouseover." + pluginName, overBehaviour);
  1615. $(textElem.node).on("mouseout." + pluginName, outBehaviour);
  1616. }
  1617. },
  1618. /*
  1619. * Remove the hover behavior for plots and areas
  1620. * @param mapElem the map element
  1621. * @param textElem the optional text element (within the map element)
  1622. */
  1623. unsetHover: function (mapElem, textElem) {
  1624. $(mapElem.node).off("." + pluginName);
  1625. if (textElem) $(textElem.node).off("." + pluginName);
  1626. },
  1627. /*
  1628. * Set he behaviour for "mouseover" event
  1629. * @param mapElem mapElem the map element
  1630. * @param textElem the optional text element (within the map element)
  1631. */
  1632. elemHover: function (mapElem, textElem) {
  1633. var self = this;
  1634. // Set mapElem
  1635. if (mapElem.attrsHover.animDuration > 0) mapElem.animate(mapElem.attrsHover, mapElem.attrsHover.animDuration);
  1636. else mapElem.attr(mapElem.attrsHover);
  1637. // Set textElem
  1638. if (textElem) {
  1639. if (textElem.attrsHover.animDuration > 0) textElem.animate(textElem.attrsHover, textElem.attrsHover.animDuration);
  1640. else textElem.attr(textElem.attrsHover);
  1641. }
  1642. // workaround for older version of Raphael
  1643. if (self.paper.safari) self.paper.safari();
  1644. },
  1645. /*
  1646. * Set he behaviour for "mouseout" event
  1647. * @param mapElem the map element
  1648. * @param textElem the optional text element (within the map element)
  1649. */
  1650. elemOut: function (mapElem, textElem) {
  1651. var self = this;
  1652. // Set mapElem
  1653. if (mapElem.attrsHover.animDuration > 0) mapElem.animate(mapElem.originalAttrs, mapElem.attrsHover.animDuration);
  1654. else mapElem.attr(mapElem.originalAttrs);
  1655. // Set textElem
  1656. if (textElem) {
  1657. if (textElem.attrsHover.animDuration > 0) textElem.animate(textElem.originalAttrs, textElem.attrsHover.animDuration);
  1658. else textElem.attr(textElem.originalAttrs);
  1659. }
  1660. // workaround for older version of Raphael
  1661. if (self.paper.safari) self.paper.safari();
  1662. },
  1663. /*
  1664. * Get element options by merging default options, element options and legend options
  1665. * @param defaultOptions
  1666. * @param elemOptions
  1667. * @param legendOptions
  1668. */
  1669. getElemOptions: function (defaultOptions, elemOptions, legendOptions) {
  1670. var self = this;
  1671. var options = $.extend(true, {}, defaultOptions, elemOptions);
  1672. if (options.value !== undefined) {
  1673. if ($.isArray(legendOptions)) {
  1674. for (var i = 0, length = legendOptions.length; i < length; ++i) {
  1675. options = $.extend(true, {}, options, self.getLegendSlice(options.value[i], legendOptions[i]));
  1676. }
  1677. } else {
  1678. options = $.extend(true, {}, options, self.getLegendSlice(options.value, legendOptions));
  1679. }
  1680. }
  1681. return options;
  1682. },
  1683. /*
  1684. * Get the coordinates of the text relative to a bbox and a position
  1685. * @param bbox the boundary box of the element
  1686. * @param textPosition the wanted text position (inner, right, left, top or bottom)
  1687. * @param margin number or object {x: val, y:val} margin between the bbox and the text
  1688. */
  1689. getTextPosition: function (bbox, textPosition, margin) {
  1690. var textX = 0;
  1691. var textY = 0;
  1692. var textAnchor = "";
  1693. if (typeof margin === "number") {
  1694. if (textPosition === "bottom" || textPosition === "top") {
  1695. margin = {x: 0, y: margin};
  1696. } else if (textPosition === "right" || textPosition === "left") {
  1697. margin = {x: margin, y: 0};
  1698. } else {
  1699. margin = {x: 0, y: 0};
  1700. }
  1701. }
  1702. switch (textPosition) {
  1703. case "bottom" :
  1704. textX = ((bbox.x + bbox.x2) / 2) + margin.x;
  1705. textY = bbox.y2 + margin.y;
  1706. textAnchor = "middle";
  1707. break;
  1708. case "top" :
  1709. textX = ((bbox.x + bbox.x2) / 2) + margin.x;
  1710. textY = bbox.y - margin.y;
  1711. textAnchor = "middle";
  1712. break;
  1713. case "left" :
  1714. textX = bbox.x - margin.x;
  1715. textY = ((bbox.y + bbox.y2) / 2) + margin.y;
  1716. textAnchor = "end";
  1717. break;
  1718. case "right" :
  1719. textX = bbox.x2 + margin.x;
  1720. textY = ((bbox.y + bbox.y2) / 2) + margin.y;
  1721. textAnchor = "start";
  1722. break;
  1723. default : // "inner" position
  1724. textX = ((bbox.x + bbox.x2) / 2) + margin.x;
  1725. textY = ((bbox.y + bbox.y2) / 2) + margin.y;
  1726. textAnchor = "middle";
  1727. }
  1728. return {"x": textX, "y": textY, "textAnchor": textAnchor};
  1729. },
  1730. /*
  1731. * Get the legend conf matching with the value
  1732. * @param value the value to match with a slice in the legend
  1733. * @param legend the legend params object
  1734. * @return the legend slice matching with the value
  1735. */
  1736. getLegendSlice: function (value, legend) {
  1737. for (var i = 0, length = legend.slices.length; i < length; ++i) {
  1738. if ((legend.slices[i].sliceValue !== undefined && value == legend.slices[i].sliceValue)
  1739. || ((legend.slices[i].sliceValue === undefined)
  1740. && (legend.slices[i].min === undefined || value >= legend.slices[i].min)
  1741. && (legend.slices[i].max === undefined || value <= legend.slices[i].max))
  1742. ) {
  1743. return legend.slices[i];
  1744. }
  1745. }
  1746. return {};
  1747. },
  1748. /*
  1749. * Animated view box changes
  1750. * As from http://code.voidblossom.com/animating-viewbox-easing-formulas/,
  1751. * (from https://github.com/theshaun works on mapael)
  1752. * @param x coordinate of the point to focus on
  1753. * @param y coordinate of the point to focus on
  1754. * @param w map defined width
  1755. * @param h map defined height
  1756. * @param duration defined length of time for animation
  1757. * @param easingFunction defined Raphael supported easing_formula to use
  1758. * @param callback method when animated action is complete
  1759. */
  1760. animateViewBox: function (x, y, w, h, duration, easingFunction) {
  1761. var self = this;
  1762. var cx = self.paper._viewBox ? self.paper._viewBox[0] : 0;
  1763. var dx = x - cx;
  1764. var cy = self.paper._viewBox ? self.paper._viewBox[1] : 0;
  1765. var dy = y - cy;
  1766. var cw = self.paper._viewBox ? self.paper._viewBox[2] : self.paper.width;
  1767. var dw = w - cw;
  1768. var ch = self.paper._viewBox ? self.paper._viewBox[3] : self.paper.height;
  1769. var dh = h - ch;
  1770. var interval = 25;
  1771. var steps = duration / interval;
  1772. var currentStep = 0;
  1773. var easingFormula;
  1774. easingFunction = easingFunction || "linear";
  1775. easingFormula = Raphael.easing_formulas[easingFunction];
  1776. clearInterval(self.animationIntervalID);
  1777. self.animationIntervalID = setInterval(function () {
  1778. var ratio = currentStep / steps;
  1779. self.paper.setViewBox(cx + dx * easingFormula(ratio),
  1780. cy + dy * easingFormula(ratio),
  1781. cw + dw * easingFormula(ratio),
  1782. ch + dh * easingFormula(ratio), false);
  1783. if (currentStep++ >= steps) {
  1784. clearInterval(self.animationIntervalID);
  1785. clearTimeout(self.zoomTO);
  1786. self.zoomTO = setTimeout(function () {
  1787. self.$map.trigger("afterZoom", {x1: x, y1: y, x2: (x + w), y2: (y + h)});
  1788. }, 150);
  1789. }
  1790. }, interval
  1791. );
  1792. },
  1793. /*
  1794. * Check for Raphael bug regarding drawing while beeing hidden (under display:none)
  1795. * See https://github.com/neveldo/jQuery-Mapael/issues/135
  1796. * @return true/false
  1797. *
  1798. * Wants to override this behavior? Use prototype overriding:
  1799. * $.mapael.prototype.isRaphaelBBoxBugPresent = function() {return false;};
  1800. */
  1801. isRaphaelBBoxBugPresent: function(){
  1802. var self = this;
  1803. // Draw text, then get its boundaries
  1804. var text_elem = self.paper.text(-50, -50, "TEST");
  1805. var text_elem_bbox = text_elem.getBBox();
  1806. // remove element
  1807. text_elem.remove();
  1808. // If it has no height and width, then the paper is hidden
  1809. return (text_elem_bbox.width === 0 && text_elem_bbox.height === 0);
  1810. },
  1811. // Default map options
  1812. defaultOptions: {
  1813. map: {
  1814. cssClass: "map",
  1815. tooltip: {
  1816. cssClass: "mapTooltip"
  1817. },
  1818. defaultArea: {
  1819. attrs: {
  1820. fill: "#343434",
  1821. stroke: "#5d5d5d",
  1822. "stroke-width": 1,
  1823. "stroke-linejoin": "round"
  1824. },
  1825. attrsHover: {
  1826. fill: "#f38a03",
  1827. animDuration: 300
  1828. },
  1829. text: {
  1830. position: "inner",
  1831. margin: 10,
  1832. attrs: {
  1833. "font-size": 15,
  1834. fill: "#c7c7c7"
  1835. },
  1836. attrsHover: {
  1837. fill: "#eaeaea",
  1838. "animDuration": 300
  1839. }
  1840. },
  1841. target: "_self"
  1842. },
  1843. defaultPlot: {
  1844. type: "circle",
  1845. size: 15,
  1846. attrs: {
  1847. fill: "#0088db",
  1848. stroke: "#fff",
  1849. "stroke-width": 0,
  1850. "stroke-linejoin": "round"
  1851. },
  1852. attrsHover: {
  1853. "stroke-width": 3,
  1854. animDuration: 300
  1855. },
  1856. text: {
  1857. position: "right",
  1858. margin: 10,
  1859. attrs: {
  1860. "font-size": 15,
  1861. fill: "#c7c7c7"
  1862. },
  1863. attrsHover: {
  1864. fill: "#eaeaea",
  1865. animDuration: 300
  1866. }
  1867. },
  1868. target: "_self"
  1869. },
  1870. defaultLink: {
  1871. factor: 0.5,
  1872. attrs: {
  1873. stroke: "#0088db",
  1874. "stroke-width": 2
  1875. },
  1876. attrsHover: {
  1877. animDuration: 300
  1878. },
  1879. text: {
  1880. position: "inner",
  1881. margin: 10,
  1882. attrs: {
  1883. "font-size": 15,
  1884. fill: "#c7c7c7"
  1885. },
  1886. attrsHover: {
  1887. fill: "#eaeaea",
  1888. animDuration: 300
  1889. }
  1890. },
  1891. target: "_self"
  1892. },
  1893. zoom: {
  1894. enabled: false,
  1895. minLevel: 0,
  1896. maxLevel: 10,
  1897. step: 0.25,
  1898. mousewheel: true,
  1899. touch: true,
  1900. animDuration: 200,
  1901. animEasing: "linear",
  1902. buttons: {
  1903. "reset": {
  1904. cssClass: "zoomButton zoomReset",
  1905. content: "&#8226;", // bullet sign
  1906. title: "Reset zoom"
  1907. },
  1908. "in": {
  1909. cssClass: "zoomButton zoomIn",
  1910. content: "+",
  1911. title: "Zoom in"
  1912. },
  1913. "out": {
  1914. cssClass: "zoomButton zoomOut",
  1915. content: "&#8722;", // minus sign
  1916. title: "Zoom out"
  1917. }
  1918. }
  1919. }
  1920. },
  1921. legend: {
  1922. area: [],
  1923. plot: []
  1924. },
  1925. areas: {},
  1926. plots: {},
  1927. links: {}
  1928. },
  1929. // Default legends option
  1930. legendDefaultOptions: {
  1931. area: {
  1932. cssClass: "areaLegend",
  1933. display: true,
  1934. marginLeft: 10,
  1935. marginLeftTitle: 5,
  1936. marginBottomTitle: 10,
  1937. marginLeftLabel: 10,
  1938. marginBottom: 10,
  1939. titleAttrs: {
  1940. "font-size": 16,
  1941. fill: "#343434",
  1942. "text-anchor": "start"
  1943. },
  1944. labelAttrs: {
  1945. "font-size": 12,
  1946. fill: "#343434",
  1947. "text-anchor": "start"
  1948. },
  1949. labelAttrsHover: {
  1950. fill: "#787878",
  1951. animDuration: 300
  1952. },
  1953. hideElemsOnClick: {
  1954. enabled: true,
  1955. opacity: 0.2,
  1956. animDuration: 300
  1957. },
  1958. slices: [],
  1959. mode: "vertical"
  1960. },
  1961. plot: {
  1962. cssClass: "plotLegend",
  1963. display: true,
  1964. marginLeft: 10,
  1965. marginLeftTitle: 5,
  1966. marginBottomTitle: 10,
  1967. marginLeftLabel: 10,
  1968. marginBottom: 10,
  1969. titleAttrs: {
  1970. "font-size": 16,
  1971. fill: "#343434",
  1972. "text-anchor": "start"
  1973. },
  1974. labelAttrs: {
  1975. "font-size": 12,
  1976. fill: "#343434",
  1977. "text-anchor": "start"
  1978. },
  1979. labelAttrsHover: {
  1980. fill: "#787878",
  1981. animDuration: 300
  1982. },
  1983. hideElemsOnClick: {
  1984. enabled: true,
  1985. opacity: 0.2,
  1986. animDuration: 300
  1987. },
  1988. slices: [],
  1989. mode: "vertical"
  1990. }
  1991. }
  1992. };
  1993. // Extend jQuery with Mapael
  1994. if ($[pluginName] === undefined) $[pluginName] = Mapael;
  1995. // Add jQuery DOM function
  1996. $.fn[pluginName] = function (options) {
  1997. // Call Mapael on each element
  1998. return this.each(function () {
  1999. // Avoid leaking problem on multiple instanciation by removing an old mapael object on a container
  2000. if ($.data(this, pluginName)) {
  2001. $.data(this, pluginName).destroy();
  2002. }
  2003. // Create Mapael and save it as jQuery data
  2004. // This allow external access to Mapael using $(".mapcontainer").data("mapael")
  2005. $.data(this, pluginName, new Mapael(this, options));
  2006. });
  2007. };
  2008. }));