UNPKG

jquery-mapael

Version:

jQuery Mapael is a jQuery plugin based on raphael.js that allows you to display dynamic vector maps.

1,154 lines (994 loc) 123 kB
/*! * * Jquery Mapael - Dynamic maps jQuery plugin (based on raphael.js) * Requires jQuery, raphael.js and jquery.mousewheel * * Version: 2.2.0 * * Copyright (c) 2017 Vincent Brouté (https://www.vincentbroute.fr/mapael) * Licensed under the MIT license (http://www.opensource.org/licenses/mit-license.php). * * Thanks to Indigo744 * */ (function (factory) { if (typeof exports === 'object') { // CommonJS module.exports = factory(require('jquery'), require('raphael'), require('jquery-mousewheel')); } else if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(['jquery', 'raphael', 'mousewheel'], factory); } else { // Browser globals factory(jQuery, Raphael, jQuery.fn.mousewheel); } }(function ($, Raphael, mousewheel, undefined) { "use strict"; // The plugin name (used on several places) var pluginName = "mapael"; // Version number of jQuery Mapael. See http://semver.org/ for more information. var version = "2.2.0"; /* * Mapael constructor * Init instance vars and call init() * @param container the DOM element on which to apply the plugin * @param options the complete options to use */ var Mapael = function (container, options) { var self = this; // the global container (DOM element object) self.container = container; // the global container (jQuery object) self.$container = $(container); // the global options self.options = self.extendDefaultOptions(options); // zoom TimeOut handler (used to set and clear) self.zoomTO = 0; // zoom center coordinate (set at touchstart) self.zoomCenterX = 0; self.zoomCenterY = 0; // Zoom pinch (set at touchstart and touchmove) self.previousPinchDist = 0; // Zoom data self.zoomData = { zoomLevel: 0, zoomX: 0, zoomY: 0, panX: 0, panY: 0 }; self.currentViewBox = { x: 0, y: 0, w: 0, h: 0 }; // Panning: tell if panning action is in progress self.panning = false; // Animate view box self.zoomAnimID = null; // Interval handler (used to set and clear) self.zoomAnimStartTime = null; // Animation start time self.zoomAnimCVBTarget = null; // Current ViewBox target // Map subcontainer jQuery object self.$map = $("." + self.options.map.cssClass, self.container); // Save initial HTML content (used by destroy method) self.initialMapHTMLContent = self.$map.html(); // The tooltip jQuery object self.$tooltip = {}; // The paper Raphael object self.paper = {}; // The areas object list self.areas = {}; // The plots object list self.plots = {}; // The links object list self.links = {}; // The legends list self.legends = {}; // The map configuration object (taken from map file) self.mapConf = {}; // Holds all custom event handlers self.customEventHandlers = {}; // Let's start the initialization self.init(); }; /* * Mapael Prototype * Defines all methods and properties needed by Mapael * Each mapael object inherits their properties and methods from this prototype */ Mapael.prototype = { /* Filtering TimeOut value in ms * Used for mouseover trigger over elements */ MouseOverFilteringTO: 120, /* Filtering TimeOut value in ms * Used for afterPanning trigger when panning */ panningFilteringTO: 150, /* Filtering TimeOut value in ms * Used for mouseup/touchend trigger when panning */ panningEndFilteringTO: 50, /* Filtering TimeOut value in ms * Used for afterZoom trigger when zooming */ zoomFilteringTO: 150, /* Filtering TimeOut value in ms * Used for when resizing window */ resizeFilteringTO: 150, /* * Initialize the plugin * Called by the constructor */ init: function () { var self = this; // Init check for class existence if (self.options.map.cssClass === "" || $("." + self.options.map.cssClass, self.container).length === 0) { throw new Error("The map class `" + self.options.map.cssClass + "` doesn't exists"); } // Create the tooltip container self.$tooltip = $("<div>").addClass(self.options.map.tooltip.cssClass).css("display", "none"); // Get the map container, empty it then append tooltip self.$map.empty().append(self.$tooltip); // Get the map from $.mapael or $.fn.mapael (backward compatibility) if ($[pluginName] && $[pluginName].maps && $[pluginName].maps[self.options.map.name]) { // Mapael version >= 2.x self.mapConf = $[pluginName].maps[self.options.map.name]; } else if ($.fn[pluginName] && $.fn[pluginName].maps && $.fn[pluginName].maps[self.options.map.name]) { // Mapael version <= 1.x - DEPRECATED self.mapConf = $.fn[pluginName].maps[self.options.map.name]; if (window.console && window.console.warn) { window.console.warn("Extending $.fn.mapael is deprecated (map '" + self.options.map.name + "')"); } } else { throw new Error("Unknown map '" + self.options.map.name + "'"); } // Create Raphael paper self.paper = new Raphael(self.$map[0], self.mapConf.width, self.mapConf.height); // issue #135: Check for Raphael bug on text element boundaries if (self.isRaphaelBBoxBugPresent() === true) { self.destroy(); throw new Error("Can't get boundary box for text (is your container hidden? See #135)"); } // add plugin class name on element self.$container.addClass(pluginName); if (self.options.map.tooltip.css) self.$tooltip.css(self.options.map.tooltip.css); self.setViewBox(0, 0, self.mapConf.width, self.mapConf.height); // Handle map size if (self.options.map.width) { // NOT responsive: map has a fixed width self.paper.setSize(self.options.map.width, self.mapConf.height * (self.options.map.width / self.mapConf.width)); } else { // Responsive: handle resizing of the map self.initResponsiveSize(); } // Draw map areas $.each(self.mapConf.elems, function (id) { // Init area object self.areas[id] = {}; // Set area options self.areas[id].options = self.getElemOptions( self.options.map.defaultArea, (self.options.areas[id] ? self.options.areas[id] : {}), self.options.legend.area ); // draw area self.areas[id].mapElem = self.paper.path(self.mapConf.elems[id]); }); // Hook that allows to add custom processing on the map if (self.options.map.beforeInit) self.options.map.beforeInit(self.$container, self.paper, self.options); // Init map areas in a second loop // Allows text to be added after ALL areas and prevent them from being hidden $.each(self.mapConf.elems, function (id) { self.initElem(id, 'area', self.areas[id]); }); // Draw links self.links = self.drawLinksCollection(self.options.links); // Draw plots $.each(self.options.plots, function (id) { self.plots[id] = self.drawPlot(id); }); // Attach zoom event self.$container.on("zoom." + pluginName, function (e, zoomOptions) { self.onZoomEvent(e, zoomOptions); }); if (self.options.map.zoom.enabled) { // Enable zoom self.initZoom(self.mapConf.width, self.mapConf.height, self.options.map.zoom); } // Set initial zoom if (self.options.map.zoom.init !== undefined) { if (self.options.map.zoom.init.animDuration === undefined) { self.options.map.zoom.init.animDuration = 0; } self.$container.trigger("zoom", self.options.map.zoom.init); } // Create the legends for areas self.createLegends("area", self.areas, 1); // Create the legends for plots taking into account the scale of the map self.createLegends("plot", self.plots, self.paper.width / self.mapConf.width); // Attach update event self.$container.on("update." + pluginName, function (e, opt) { self.onUpdateEvent(e, opt); }); // Attach showElementsInRange event self.$container.on("showElementsInRange." + pluginName, function (e, opt) { self.onShowElementsInRange(e, opt); }); // Attach delegated events self.initDelegatedMapEvents(); // Attach delegated custom events self.initDelegatedCustomEvents(); // Hook that allows to add custom processing on the map if (self.options.map.afterInit) self.options.map.afterInit(self.$container, self.paper, self.areas, self.plots, self.options); $(self.paper.desc).append(" and Mapael " + self.version + " (https://www.vincentbroute.fr/mapael/)"); }, /* * Destroy mapael * This function effectively detach mapael from the container * - Set the container back to the way it was before mapael instanciation * - Remove all data associated to it (memory can then be free'ed by browser) * * This method can be call directly by user: * $(".mapcontainer").data("mapael").destroy(); * * This method is also automatically called if the user try to call mapael * on a container already containing a mapael instance */ destroy: function () { var self = this; // Detach all event listeners attached to the container self.$container.off("." + pluginName); self.$map.off("." + pluginName); // Detach the global resize event handler if (self.onResizeEvent) $(window).off("resize." + pluginName, self.onResizeEvent); // Empty the container (this will also detach all event listeners) self.$map.empty(); // Replace initial HTML content self.$map.html(self.initialMapHTMLContent); // Empty legend containers and replace initial HTML content $.each(self.legends, function(legendType) { $.each(self.legends[legendType], function(legendIndex) { var legend = self.legends[legendType][legendIndex]; legend.container.empty(); legend.container.html(legend.initialHTMLContent); }); }); // Remove mapael class self.$container.removeClass(pluginName); // Remove the data self.$container.removeData(pluginName); // Remove all internal reference self.container = undefined; self.$container = undefined; self.options = undefined; self.paper = undefined; self.$map = undefined; self.$tooltip = undefined; self.mapConf = undefined; self.areas = undefined; self.plots = undefined; self.links = undefined; self.customEventHandlers = undefined; }, initResponsiveSize: function () { var self = this; var resizeTO = null; // Function that actually handle the resizing var handleResize = function(isInit) { var containerWidth = self.$map.width(); if (self.paper.width !== containerWidth) { var newScale = containerWidth / self.mapConf.width; // Set new size self.paper.setSize(containerWidth, self.mapConf.height * newScale); // Create plots legend again to take into account the new scale // Do not do this on init (it will be done later) if (isInit !== true && self.options.legend.redrawOnResize) { self.createLegends("plot", self.plots, newScale); } } }; self.onResizeEvent = function() { // Clear any previous setTimeout (avoid too much triggering) clearTimeout(resizeTO); // setTimeout to wait for the user to finish its resizing resizeTO = setTimeout(function () { handleResize(); }, self.resizeFilteringTO); }; // Attach resize handler $(window).on("resize." + pluginName, self.onResizeEvent); // Call once handleResize(true); }, /* * Extend the user option with the default one * @param options the user options * @return new options object */ extendDefaultOptions: function (options) { // Extend default options with user options options = $.extend(true, {}, Mapael.prototype.defaultOptions, options); // Extend legend default options $.each(['area', 'plot'], function (key, type) { if ($.isArray(options.legend[type])) { for (var i = 0; i < options.legend[type].length; ++i) options.legend[type][i] = $.extend(true, {}, Mapael.prototype.legendDefaultOptions[type], options.legend[type][i]); } else { options.legend[type] = $.extend(true, {}, Mapael.prototype.legendDefaultOptions[type], options.legend[type]); } }); return options; }, /* * Init all delegated events for the whole map: * mouseover * mousemove * mouseout */ initDelegatedMapEvents: function() { var self = this; // Mapping between data-type value and the corresponding elements array // Note: legend-elem and legend-label are not in this table because // they need a special processing var dataTypeToElementMapping = { 'area' : self.areas, 'area-text' : self.areas, 'plot' : self.plots, 'plot-text' : self.plots, 'link' : self.links, 'link-text' : self.links }; /* Attach mouseover event delegation * Note: we filter the event with a timeout to reduce the firing when the mouse moves quickly */ var mapMouseOverTimeoutID; self.$container.on("mouseover." + pluginName, "[data-id]", function () { var elem = this; clearTimeout(mapMouseOverTimeoutID); mapMouseOverTimeoutID = setTimeout(function() { var $elem = $(elem); var id = $elem.attr('data-id'); var type = $elem.attr('data-type'); if (dataTypeToElementMapping[type] !== undefined) { self.elemEnter(dataTypeToElementMapping[type][id]); } else if (type === 'legend-elem' || type === 'legend-label') { var legendIndex = $elem.attr('data-legend-id'); var legendType = $elem.attr('data-legend-type'); self.elemEnter(self.legends[legendType][legendIndex].elems[id]); } }, self.MouseOverFilteringTO); }); /* Attach mousemove event delegation * Note: timeout filtering is small to update the Tooltip position fast */ var mapMouseMoveTimeoutID; self.$container.on("mousemove." + pluginName, "[data-id]", function (event) { var elem = this; clearTimeout(mapMouseMoveTimeoutID); mapMouseMoveTimeoutID = setTimeout(function() { var $elem = $(elem); var id = $elem.attr('data-id'); var type = $elem.attr('data-type'); if (dataTypeToElementMapping[type] !== undefined) { self.elemHover(dataTypeToElementMapping[type][id], event); } else if (type === 'legend-elem' || type === 'legend-label') { /* Nothing to do */ } }, 0); }); /* Attach mouseout event delegation * Note: we don't perform any timeout filtering to clear & reset elem ASAP * Otherwise an element may be stuck in 'hover' state (which is NOT good) */ self.$container.on("mouseout." + pluginName, "[data-id]", function () { var elem = this; // Clear any clearTimeout(mapMouseOverTimeoutID); clearTimeout(mapMouseMoveTimeoutID); var $elem = $(elem); var id = $elem.attr('data-id'); var type = $elem.attr('data-type'); if (dataTypeToElementMapping[type] !== undefined) { self.elemOut(dataTypeToElementMapping[type][id]); } else if (type === 'legend-elem' || type === 'legend-label') { var legendIndex = $elem.attr('data-legend-id'); var legendType = $elem.attr('data-legend-type'); self.elemOut(self.legends[legendType][legendIndex].elems[id]); } }); /* Attach click event delegation * Note: we filter the event with a timeout to avoid double click */ self.$container.on("click." + pluginName, "[data-id]", function (evt, opts) { var $elem = $(this); var id = $elem.attr('data-id'); var type = $elem.attr('data-type'); if (dataTypeToElementMapping[type] !== undefined) { self.elemClick(dataTypeToElementMapping[type][id]); } else if (type === 'legend-elem' || type === 'legend-label') { var legendIndex = $elem.attr('data-legend-id'); var legendType = $elem.attr('data-legend-type'); self.handleClickOnLegendElem(self.legends[legendType][legendIndex].elems[id], id, legendIndex, legendType, opts); } }); }, /* * Init all delegated custom events */ initDelegatedCustomEvents: function() { var self = this; $.each(self.customEventHandlers, function(eventName) { // Namespace the custom event // This allow to easily unbound only custom events and not regular ones var fullEventName = eventName + '.' + pluginName + ".custom"; self.$container.off(fullEventName).on(fullEventName, "[data-id]", function (e) { var $elem = $(this); var id = $elem.attr('data-id'); var type = $elem.attr('data-type').replace('-text', ''); if (!self.panning && self.customEventHandlers[eventName][type] !== undefined && self.customEventHandlers[eventName][type][id] !== undefined) { // Get back related elem var elem = self.customEventHandlers[eventName][type][id]; // Run callback provided by user elem.options.eventHandlers[eventName](e, id, elem.mapElem, elem.textElem, elem.options); } }); }); }, /* * Init the element "elem" on the map (drawing text, setting attributes, events, tooltip, ...) * * @param id the id of the element * @param type the type of the element (area, plot, link) * @param elem object the element object (with mapElem), it will be updated */ initElem: function (id, type, elem) { var self = this; var $mapElem = $(elem.mapElem.node); // If an HTML link exists for this element, add cursor attributes if (elem.options.href) { elem.options.attrs.cursor = "pointer"; if (elem.options.text) elem.options.text.attrs.cursor = "pointer"; } // Set SVG attributes to map element elem.mapElem.attr(elem.options.attrs); // Set DOM attributes to map element $mapElem.attr({ "data-id": id, "data-type": type }); if (elem.options.cssClass !== undefined) { $mapElem.addClass(elem.options.cssClass); } // Init the label related to the element if (elem.options.text && elem.options.text.content !== undefined) { // Set a text label in the area var textPosition = self.getTextPosition(elem.mapElem.getBBox(), elem.options.text.position, elem.options.text.margin); elem.options.text.attrs.text = elem.options.text.content; elem.options.text.attrs.x = textPosition.x; elem.options.text.attrs.y = textPosition.y; elem.options.text.attrs['text-anchor'] = textPosition.textAnchor; // Draw text elem.textElem = self.paper.text(textPosition.x, textPosition.y, elem.options.text.content); // Apply SVG attributes to text element elem.textElem.attr(elem.options.text.attrs); // Apply DOM attributes $(elem.textElem.node).attr({ "data-id": id, "data-type": type + '-text' }); } // Set user event handlers if (elem.options.eventHandlers) self.setEventHandlers(id, type, elem); // Set hover option for mapElem self.setHoverOptions(elem.mapElem, elem.options.attrs, elem.options.attrsHover); // Set hover option for textElem if (elem.textElem) self.setHoverOptions(elem.textElem, elem.options.text.attrs, elem.options.text.attrsHover); }, /* * Init zoom and panning for the map * @param mapWidth * @param mapHeight * @param zoomOptions */ initZoom: function (mapWidth, mapHeight, zoomOptions) { var self = this; var mousedown = false; var previousX = 0; var previousY = 0; var fnZoomButtons = { "reset": function () { self.$container.trigger("zoom", {"level": 0}); }, "in": function () { self.$container.trigger("zoom", {"level": "+1"}); }, "out": function () { self.$container.trigger("zoom", {"level": -1}); } }; // init Zoom data $.extend(self.zoomData, { zoomLevel: 0, panX: 0, panY: 0 }); // init zoom buttons $.each(zoomOptions.buttons, function(type, opt) { if (fnZoomButtons[type] === undefined) throw new Error("Unknown zoom button '" + type + "'"); // Create div with classes, contents and title (for tooltip) var $button = $("<div>").addClass(opt.cssClass) .html(opt.content) .attr("title", opt.title); // Assign click event $button.on("click." + pluginName, fnZoomButtons[type]); // Append to map self.$map.append($button); }); // Update the zoom level of the map on mousewheel if (self.options.map.zoom.mousewheel) { self.$map.on("mousewheel." + pluginName, function (e) { var zoomLevel = (e.deltaY > 0) ? 1 : -1; var coord = self.mapPagePositionToXY(e.pageX, e.pageY); self.$container.trigger("zoom", { "fixedCenter": true, "level": self.zoomData.zoomLevel + zoomLevel, "x": coord.x, "y": coord.y }); e.preventDefault(); }); } // Update the zoom level of the map on touch pinch if (self.options.map.zoom.touch) { self.$map.on("touchstart." + pluginName, function (e) { if (e.originalEvent.touches.length === 2) { self.zoomCenterX = (e.originalEvent.touches[0].pageX + e.originalEvent.touches[1].pageX) / 2; self.zoomCenterY = (e.originalEvent.touches[0].pageY + e.originalEvent.touches[1].pageY) / 2; 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)); } }); self.$map.on("touchmove." + pluginName, function (e) { var pinchDist = 0; var zoomLevel = 0; if (e.originalEvent.touches.length === 2) { 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)); if (Math.abs(pinchDist - self.previousPinchDist) > 15) { var coord = self.mapPagePositionToXY(self.zoomCenterX, self.zoomCenterY); zoomLevel = (pinchDist - self.previousPinchDist) / Math.abs(pinchDist - self.previousPinchDist); self.$container.trigger("zoom", { "fixedCenter": true, "level": self.zoomData.zoomLevel + zoomLevel, "x": coord.x, "y": coord.y }); self.previousPinchDist = pinchDist; } return false; } }); } // When the user drag the map, prevent to move the clicked element instead of dragging the map (behaviour seen with Firefox) self.$map.on("dragstart", function() { return false; }); // Panning var panningMouseUpTO = null; var panningMouseMoveTO = null; $("body").on("mouseup." + pluginName + (zoomOptions.touch ? " touchend." + pluginName : ""), function () { mousedown = false; clearTimeout(panningMouseUpTO); clearTimeout(panningMouseMoveTO); panningMouseUpTO = setTimeout(function () { self.panning = false; }, self.panningEndFilteringTO); }); self.$map.on("mousedown." + pluginName + (zoomOptions.touch ? " touchstart." + pluginName : ""), function (e) { clearTimeout(panningMouseUpTO); clearTimeout(panningMouseMoveTO); if (e.pageX !== undefined) { mousedown = true; previousX = e.pageX; previousY = e.pageY; } else { if (e.originalEvent.touches.length === 1) { mousedown = true; previousX = e.originalEvent.touches[0].pageX; previousY = e.originalEvent.touches[0].pageY; } } }).on("mousemove." + pluginName + (zoomOptions.touch ? " touchmove." + pluginName : ""), function (e) { var currentLevel = self.zoomData.zoomLevel; var pageX = 0; var pageY = 0; clearTimeout(panningMouseUpTO); clearTimeout(panningMouseMoveTO); if (e.pageX !== undefined) { pageX = e.pageX; pageY = e.pageY; } else { if (e.originalEvent.touches.length === 1) { pageX = e.originalEvent.touches[0].pageX; pageY = e.originalEvent.touches[0].pageY; } else { mousedown = false; } } if (mousedown && currentLevel !== 0) { var offsetX = (previousX - pageX) / (1 + (currentLevel * zoomOptions.step)) * (mapWidth / self.paper.width); var offsetY = (previousY - pageY) / (1 + (currentLevel * zoomOptions.step)) * (mapHeight / self.paper.height); var panX = Math.min(Math.max(0, self.currentViewBox.x + offsetX), (mapWidth - self.currentViewBox.w)); var panY = Math.min(Math.max(0, self.currentViewBox.y + offsetY), (mapHeight - self.currentViewBox.h)); if (Math.abs(offsetX) > 5 || Math.abs(offsetY) > 5) { $.extend(self.zoomData, { panX: panX, panY: panY, zoomX: panX + self.currentViewBox.w / 2, zoomY: panY + self.currentViewBox.h / 2 }); self.setViewBox(panX, panY, self.currentViewBox.w, self.currentViewBox.h); panningMouseMoveTO = setTimeout(function () { self.$map.trigger("afterPanning", { x1: panX, y1: panY, x2: (panX + self.currentViewBox.w), y2: (panY + self.currentViewBox.h) }); }, self.panningFilteringTO); previousX = pageX; previousY = pageY; self.panning = true; } return false; } }); }, /* * Map a mouse position to a map position * Transformation principle: * ** start with (pageX, pageY) absolute mouse coordinate * - Apply translation: take into accounts the map offset in the page * ** from this point, we have relative mouse coordinate * - Apply homothetic transformation: take into accounts initial factor of map sizing (fullWidth / actualWidth) * - Apply homothetic transformation: take into accounts the zoom factor * ** from this point, we have relative map coordinate * - Apply translation: take into accounts the current panning of the map * ** from this point, we have absolute map coordinate * @param pageX: mouse client coordinate on X * @param pageY: mouse client coordinate on Y * @return map coordinate {x, y} */ mapPagePositionToXY: function(pageX, pageY) { var self = this; var offset = self.$map.offset(); var initFactor = (self.options.map.width) ? (self.mapConf.width / self.options.map.width) : (self.mapConf.width / self.$map.width()); var zoomFactor = 1 / (1 + (self.zoomData.zoomLevel * self.options.map.zoom.step)); return { x: (zoomFactor * initFactor * (pageX - offset.left)) + self.zoomData.panX, y: (zoomFactor * initFactor * (pageY - offset.top)) + self.zoomData.panY }; }, /* * Zoom on the map * * zoomOptions.animDuration zoom duration * * zoomOptions.level level of the zoom between minLevel and maxLevel (absolute number, or relative string +1 or -1) * zoomOptions.fixedCenter set to true in order to preserve the position of x,y in the canvas when zoomed * * zoomOptions.x x coordinate of the point to focus on * zoomOptions.y y coordinate of the point to focus on * - OR - * zoomOptions.latitude latitude of the point to focus on * zoomOptions.longitude longitude of the point to focus on * - OR - * zoomOptions.plot plot ID to focus on * - OR - * zoomOptions.area area ID to focus on * zoomOptions.areaMargin margin (in pixels) around the area * * If an area ID is specified, the algorithm will override the zoom level to focus on the area * but it may be limited by the min/max zoom level limits set at initialization. * * If no coordinates are specified, the zoom will be focused on the center of the current view box * */ onZoomEvent: function (e, zoomOptions) { var self = this; // new Top/Left corner coordinates var panX; var panY; // new Width/Height viewbox size var panWidth; var panHeight; // Zoom level in absolute scale (from 0 to max, by step of 1) var zoomLevel = self.zoomData.zoomLevel; // Relative zoom level (from 1 to max, by step of 0.25 (default)) var previousRelativeZoomLevel = 1 + self.zoomData.zoomLevel * self.options.map.zoom.step; var relativeZoomLevel; var animDuration = (zoomOptions.animDuration !== undefined) ? zoomOptions.animDuration : self.options.map.zoom.animDuration; if (zoomOptions.area !== undefined) { /* An area is given * We will define x/y coordinate AND a new zoom level to fill the area */ if (self.areas[zoomOptions.area] === undefined) throw new Error("Unknown area '" + zoomOptions.area + "'"); var areaMargin = (zoomOptions.areaMargin !== undefined) ? zoomOptions.areaMargin : 10; var areaBBox = self.areas[zoomOptions.area].mapElem.getBBox(); var areaFullWidth = areaBBox.width + 2 * areaMargin; var areaFullHeight = areaBBox.height + 2 * areaMargin; // Compute new x/y focus point (center of area) zoomOptions.x = areaBBox.cx; zoomOptions.y = areaBBox.cy; // Compute a new absolute zoomLevel value (inverse of relative -> absolute) // Take the min between zoomLevel on width vs. height to be able to see the whole area zoomLevel = Math.min(Math.floor((self.mapConf.width / areaFullWidth - 1) / self.options.map.zoom.step), Math.floor((self.mapConf.height / areaFullHeight - 1) / self.options.map.zoom.step)); } else { // Get user defined zoom level if (zoomOptions.level !== undefined) { if (typeof zoomOptions.level === "string") { // level is a string, either "n", "+n" or "-n" if ((zoomOptions.level.slice(0, 1) === '+') || (zoomOptions.level.slice(0, 1) === '-')) { // zoomLevel is relative zoomLevel = self.zoomData.zoomLevel + parseInt(zoomOptions.level, 10); } else { // zoomLevel is absolute zoomLevel = parseInt(zoomOptions.level, 10); } } else { // level is integer if (zoomOptions.level < 0) { // zoomLevel is relative zoomLevel = self.zoomData.zoomLevel + zoomOptions.level; } else { // zoomLevel is absolute zoomLevel = zoomOptions.level; } } } if (zoomOptions.plot !== undefined) { if (self.plots[zoomOptions.plot] === undefined) throw new Error("Unknown plot '" + zoomOptions.plot + "'"); zoomOptions.x = self.plots[zoomOptions.plot].coords.x; zoomOptions.y = self.plots[zoomOptions.plot].coords.y; } else { if (zoomOptions.latitude !== undefined && zoomOptions.longitude !== undefined) { var coords = self.mapConf.getCoords(zoomOptions.latitude, zoomOptions.longitude); zoomOptions.x = coords.x; zoomOptions.y = coords.y; } if (zoomOptions.x === undefined) { zoomOptions.x = self.currentViewBox.x + self.currentViewBox.w / 2; } if (zoomOptions.y === undefined) { zoomOptions.y = self.currentViewBox.y + self.currentViewBox.h / 2; } } } // Make sure we stay in the zoom level boundaries zoomLevel = Math.min(Math.max(zoomLevel, self.options.map.zoom.minLevel), self.options.map.zoom.maxLevel); // Compute relative zoom level relativeZoomLevel = 1 + zoomLevel * self.options.map.zoom.step; // Compute panWidth / panHeight panWidth = self.mapConf.width / relativeZoomLevel; panHeight = self.mapConf.height / relativeZoomLevel; if (zoomLevel === 0) { panX = 0; panY = 0; } else { if (zoomOptions.fixedCenter !== undefined && zoomOptions.fixedCenter === true) { panX = self.zoomData.panX + ((zoomOptions.x - self.zoomData.panX) * (relativeZoomLevel - previousRelativeZoomLevel)) / relativeZoomLevel; panY = self.zoomData.panY + ((zoomOptions.y - self.zoomData.panY) * (relativeZoomLevel - previousRelativeZoomLevel)) / relativeZoomLevel; } else { panX = zoomOptions.x - panWidth / 2; panY = zoomOptions.y - panHeight / 2; } // Make sure we stay in the map boundaries panX = Math.min(Math.max(0, panX), self.mapConf.width - panWidth); panY = Math.min(Math.max(0, panY), self.mapConf.height - panHeight); } // Update zoom level of the map if (relativeZoomLevel === previousRelativeZoomLevel && panX === self.zoomData.panX && panY === self.zoomData.panY) return; if (animDuration > 0) { self.animateViewBox(panX, panY, panWidth, panHeight, animDuration, self.options.map.zoom.animEasing); } else { self.setViewBox(panX, panY, panWidth, panHeight); clearTimeout(self.zoomTO); self.zoomTO = setTimeout(function () { self.$map.trigger("afterZoom", { x1: panX, y1: panY, x2: panX + panWidth, y2: panY + panHeight }); }, self.zoomFilteringTO); } $.extend(self.zoomData, { zoomLevel: zoomLevel, panX: panX, panY: panY, zoomX: panX + panWidth / 2, zoomY: panY + panHeight / 2 }); }, /* * Show some element in range defined by user * Triggered by user $(".mapcontainer").trigger("showElementsInRange", [opt]); * * @param opt the options * opt.hiddenOpacity opacity for hidden element (default = 0.3) * opt.animDuration animation duration in ms (default = 0) * opt.afterShowRange callback * opt.ranges the range to show: * Example: * opt.ranges = { * 'plot' : { * 0 : { // valueIndex * 'min': 1000, * 'max': 1200 * }, * 1 : { // valueIndex * 'min': 10, * 'max': 12 * } * }, * 'area' : { * {'min': 10, 'max': 20} // No valueIndex, only an object, use 0 as valueIndex (easy case) * } * } */ onShowElementsInRange: function(e, opt) { var self = this; // set animDuration to default if not defined if (opt.animDuration === undefined) { opt.animDuration = 0; } // set hiddenOpacity to default if not defined if (opt.hiddenOpacity === undefined) { opt.hiddenOpacity = 0.3; } // handle area if (opt.ranges && opt.ranges.area) { self.showElemByRange(opt.ranges.area, self.areas, opt.hiddenOpacity, opt.animDuration); } // handle plot if (opt.ranges && opt.ranges.plot) { self.showElemByRange(opt.ranges.plot, self.plots, opt.hiddenOpacity, opt.animDuration); } // handle link if (opt.ranges && opt.ranges.link) { self.showElemByRange(opt.ranges.link, self.links, opt.hiddenOpacity, opt.animDuration); } // Call user callback if (opt.afterShowRange) opt.afterShowRange(); }, /* * Show some element in range * @param ranges: the ranges * @param elems: list of element on which to check against previous range * @hiddenOpacity: the opacity when hidden * @animDuration: the animation duration */ showElemByRange: function(ranges, elems, hiddenOpacity, animDuration) { var self = this; // Hold the final opacity value for all elements consolidated after applying each ranges // This allow to set the opacity only once for each elements var elemsFinalOpacity = {}; // set object with one valueIndex to 0 if we have directly the min/max if (ranges.min !== undefined || ranges.max !== undefined) { ranges = {0: ranges}; } // Loop through each valueIndex $.each(ranges, function (valueIndex) { var range = ranges[valueIndex]; // Check if user defined at least a min or max value if (range.min === undefined && range.max === undefined) { return true; // skip this iteration (each loop), goto next range } // Loop through each elements $.each(elems, function (id) { var elemValue = elems[id].options.value; // set value with one valueIndex to 0 if not object if (typeof elemValue !== "object") { elemValue = [elemValue]; } // Check existence of this value index if (elemValue[valueIndex] === undefined) { return true; // skip this iteration (each loop), goto next element } // Check if in range if ((range.min !== undefined && elemValue[valueIndex] < range.min) || (range.max !== undefined && elemValue[valueIndex] > range.max)) { // Element not in range elemsFinalOpacity[id] = hiddenOpacity; } else { // Element in range elemsFinalOpacity[id] = 1; } }); }); // Now that we looped through all ranges, we can really assign the final opacity $.each(elemsFinalOpacity, function (id) { self.setElementOpacity(elems[id], elemsFinalOpacity[id], animDuration); }); }, /* * Set element opacity * Handle elem.mapElem and elem.textElem * @param elem the element * @param opacity the opacity to apply * @param animDuration the animation duration to use */ setElementOpacity: function(elem, opacity, animDuration) { var self = this; // Ensure no animation is running //elem.mapElem.stop(); //if (elem.textElem) elem.textElem.stop(); // If final opacity is not null, ensure element is shown before proceeding if (opacity > 0) { elem.mapElem.show(); if (elem.textElem) elem.textElem.show(); } self.animate(elem.mapElem, {"opacity": opacity}, animDuration, function () { // If final attribute is 0, hide if (opacity === 0) elem.mapElem.hide(); }); self.animate(elem.textElem, {"opacity": opacity}, animDuration, function () { // If final attribute is 0, hide if (opacity === 0) elem.textElem.hide(); }); }, /* * Update the current map * * Refresh attributes and tooltips for areas and plots * @param opt option for the refresh : * opt.mapOptions: options to update for plots and areas * opt.replaceOptions: whether mapsOptions should entirely replace current map options, or just extend it * opt.opt.newPlots new plots to add to the map * opt.newLinks new links to add to the map * opt.deletePlotKeys plots to delete from the map (array, or "all" to remove all plots) * opt.deleteLinkKeys links to remove from the map (array, or "all" to remove all links) * opt.setLegendElemsState the state of legend elements to be set : show (default) or hide * opt.animDuration animation duration in ms (default = 0) * opt.afterUpdate hook that allows to add custom processing on the map */ onUpdateEvent: function (e, opt) { var self = this; // Abort if opt is undefined if (typeof opt !== "object") return; var i = 0; var animDuration = (opt.animDuration) ? opt.animDuration : 0; // This function remove an element using animation (or not, depending on animDuration) // Used for deletePlotKeys and deleteLinkKeys var fnRemoveElement = function (elem) { self.animate(elem.mapElem, {"opacity": 0}, animDuration, function () { elem.mapElem.remove(); }); self.animate(elem.textElem, {"opacity": 0}, animDuration, function () { elem.textElem.remove(); }); }; // This function show an element using animation // Used for newPlots and newLinks var fnShowElement = function (elem) { // Starts with hidden elements elem.mapElem.attr({opacity: 0}); if (elem.textElem) elem.textElem.attr({opacity: 0}); // Set final element opacity self.setElementOpacity( elem, (elem.mapElem.originalAttrs.opacity !== undefined) ? elem.mapElem.originalAttrs.opacity : 1, animDuration ); }; if (typeof opt.mapOptions === "object") { if (opt.replaceOptions === true) self.options = self.extendDefaultOptions(opt.mapOptions); else $.extend(true, self.options, opt.mapOptions); // IF we update areas, plots or legend, then reset all legend state to "show" if (opt.mapOptions.areas !== undefined || opt.mapOptions.plots !== undefined || opt.mapOptions.legend !== undefined) { $("[data-type='legend-elem']", self.$container).each(function (id, elem) { if ($(elem).attr('data-hidden') === "1") { // Toggle state of element by clicking $(elem).trigger("click", {hideOtherElems: false, animDuration: animDuration}); } }); } } // Delete