You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2781 lines
120 KiB
2781 lines
120 KiB
/*! |
|
* |
|
* 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 plots by name if deletePlotKeys is array |
|
if (typeof opt.deletePlotKeys === "object") { |
|
for (; i < opt.deletePlotKeys.length; i++) { |
|
if (self.plots[opt.deletePlotKeys[i]] !== undefined) { |
|
fnRemoveElement(self.plots[opt.deletePlotKeys[i]]); |
|
delete self.plots[opt.deletePlotKeys[i]]; |
|
} |
|
} |
|
// Delete ALL plots if deletePlotKeys is set to "all" |
|
} else if (opt.deletePlotKeys === "all") { |
|
$.each(self.plots, function (id, elem) { |
|
fnRemoveElement(elem); |
|
}); |
|
// Empty plots object |
|
self.plots = {}; |
|
} |
|
|
|
// Delete links by name if deleteLinkKeys is array |
|
if (typeof opt.deleteLinkKeys === "object") { |
|
for (i = 0; i < opt.deleteLinkKeys.length; i++) { |
|
if (self.links[opt.deleteLinkKeys[i]] !== undefined) { |
|
fnRemoveElement(self.links[opt.deleteLinkKeys[i]]); |
|
delete self.links[opt.deleteLinkKeys[i]]; |
|
} |
|
} |
|
// Delete ALL links if deleteLinkKeys is set to "all" |
|
} else if (opt.deleteLinkKeys === "all") { |
|
$.each(self.links, function (id, elem) { |
|
fnRemoveElement(elem); |
|
}); |
|
// Empty links object |
|
self.links = {}; |
|
} |
|
|
|
// New plots |
|
if (typeof opt.newPlots === "object") { |
|
$.each(opt.newPlots, function (id) { |
|
if (self.plots[id] === undefined) { |
|
self.options.plots[id] = opt.newPlots[id]; |
|
self.plots[id] = self.drawPlot(id); |
|
if (animDuration > 0) { |
|
fnShowElement(self.plots[id]); |
|
} |
|
} |
|
}); |
|
} |
|
|
|
// New links |
|
if (typeof opt.newLinks === "object") { |
|
var newLinks = self.drawLinksCollection(opt.newLinks); |
|
$.extend(self.links, newLinks); |
|
$.extend(self.options.links, opt.newLinks); |
|
if (animDuration > 0) { |
|
$.each(newLinks, function (id) { |
|
fnShowElement(newLinks[id]); |
|
}); |
|
} |
|
} |
|
|
|
// Update areas attributes and tooltips |
|
$.each(self.areas, function (id) { |
|
// Avoid updating unchanged elements |
|
if ((typeof opt.mapOptions === "object" && |
|
( |
|
(typeof opt.mapOptions.map === "object" && typeof opt.mapOptions.map.defaultArea === "object") || |
|
(typeof opt.mapOptions.areas === "object" && typeof opt.mapOptions.areas[id] === "object") || |
|
(typeof opt.mapOptions.legend === "object" && typeof opt.mapOptions.legend.area === "object") |
|
)) || opt.replaceOptions === true |
|
) { |
|
self.areas[id].options = self.getElemOptions( |
|
self.options.map.defaultArea, |
|
(self.options.areas[id] ? self.options.areas[id] : {}), |
|
self.options.legend.area |
|
); |
|
self.updateElem(self.areas[id], animDuration); |
|
} |
|
}); |
|
|
|
// Update plots attributes and tooltips |
|
$.each(self.plots, function (id) { |
|
// Avoid updating unchanged elements |
|
if ((typeof opt.mapOptions ==="object" && |
|
( |
|
(typeof opt.mapOptions.map === "object" && typeof opt.mapOptions.map.defaultPlot === "object") || |
|
(typeof opt.mapOptions.plots === "object" && typeof opt.mapOptions.plots[id] === "object") || |
|
(typeof opt.mapOptions.legend === "object" && typeof opt.mapOptions.legend.plot === "object") |
|
)) || opt.replaceOptions === true |
|
) { |
|
self.plots[id].options = self.getElemOptions( |
|
self.options.map.defaultPlot, |
|
(self.options.plots[id] ? self.options.plots[id] : {}), |
|
self.options.legend.plot |
|
); |
|
|
|
self.setPlotCoords(self.plots[id]); |
|
self.setPlotAttributes(self.plots[id]); |
|
|
|
self.updateElem(self.plots[id], animDuration); |
|
} |
|
}); |
|
|
|
// Update links attributes and tooltips |
|
$.each(self.links, function (id) { |
|
// Avoid updating unchanged elements |
|
if ((typeof opt.mapOptions === "object" && |
|
( |
|
(typeof opt.mapOptions.map === "object" && typeof opt.mapOptions.map.defaultLink === "object") || |
|
(typeof opt.mapOptions.links === "object" && typeof opt.mapOptions.links[id] === "object") |
|
)) || opt.replaceOptions === true |
|
) { |
|
self.links[id].options = self.getElemOptions( |
|
self.options.map.defaultLink, |
|
(self.options.links[id] ? self.options.links[id] : {}), |
|
{} |
|
); |
|
|
|
self.updateElem(self.links[id], animDuration); |
|
} |
|
}); |
|
|
|
// Update legends |
|
if (opt.mapOptions && ( |
|
(typeof opt.mapOptions.legend === "object") || |
|
(typeof opt.mapOptions.map === "object" && typeof opt.mapOptions.map.defaultArea === "object") || |
|
(typeof opt.mapOptions.map === "object" && typeof opt.mapOptions.map.defaultPlot === "object") |
|
)) { |
|
// Show all elements on the map before updating the legends |
|
$("[data-type='legend-elem']", self.$container).each(function (id, elem) { |
|
if ($(elem).attr('data-hidden') === "1") { |
|
$(elem).trigger("click", {hideOtherElems: false, animDuration: animDuration}); |
|
} |
|
}); |
|
|
|
self.createLegends("area", self.areas, 1); |
|
if (self.options.map.width) { |
|
self.createLegends("plot", self.plots, (self.options.map.width / self.mapConf.width)); |
|
} else { |
|
self.createLegends("plot", self.plots, (self.$map.width() / self.mapConf.width)); |
|
} |
|
} |
|
|
|
// Hide/Show all elements based on showlegendElems |
|
// Toggle (i.e. click) only if: |
|
// - slice legend is shown AND we want to hide |
|
// - slice legend is hidden AND we want to show |
|
if (typeof opt.setLegendElemsState === "object") { |
|
// setLegendElemsState is an object listing the legend we want to hide/show |
|
$.each(opt.setLegendElemsState, function (legendCSSClass, action) { |
|
// Search for the legend |
|
var $legend = self.$container.find("." + legendCSSClass)[0]; |
|
if ($legend !== undefined) { |
|
// Select all elem inside this legend |
|
$("[data-type='legend-elem']", $legend).each(function (id, elem) { |
|
if (($(elem).attr('data-hidden') === "0" && action === "hide") || |
|
($(elem).attr('data-hidden') === "1" && action === "show")) { |
|
// Toggle state of element by clicking |
|
$(elem).trigger("click", {hideOtherElems: false, animDuration: animDuration}); |
|
} |
|
}); |
|
} |
|
}); |
|
} else { |
|
// setLegendElemsState is a string, or is undefined |
|
// Default : "show" |
|
var action = (opt.setLegendElemsState === "hide") ? "hide" : "show"; |
|
|
|
$("[data-type='legend-elem']", self.$container).each(function (id, elem) { |
|
if (($(elem).attr('data-hidden') === "0" && action === "hide") || |
|
($(elem).attr('data-hidden') === "1" && action === "show")) { |
|
// Toggle state of element by clicking |
|
$(elem).trigger("click", {hideOtherElems: false, animDuration: animDuration}); |
|
} |
|
}); |
|
} |
|
|
|
// Always rebind custom events on update |
|
self.initDelegatedCustomEvents(); |
|
|
|
if (opt.afterUpdate) opt.afterUpdate(self.$container, self.paper, self.areas, self.plots, self.options, self.links); |
|
}, |
|
|
|
/* |
|
* Set plot coordinates |
|
* @param plot object plot element |
|
*/ |
|
setPlotCoords: function(plot) { |
|
var self = this; |
|
|
|
if (plot.options.x !== undefined && plot.options.y !== undefined) { |
|
plot.coords = { |
|
x: plot.options.x, |
|
y: plot.options.y |
|
}; |
|
} else if (plot.options.plotsOn !== undefined && self.areas[plot.options.plotsOn] !== undefined) { |
|
var areaBBox = self.areas[plot.options.plotsOn].mapElem.getBBox(); |
|
plot.coords = { |
|
x: areaBBox.cx, |
|
y: areaBBox.cy |
|
}; |
|
} else { |
|
plot.coords = self.mapConf.getCoords(plot.options.latitude, plot.options.longitude); |
|
} |
|
}, |
|
|
|
/* |
|
* Set plot size attributes according to its type |
|
* Note: for SVG, plot.mapElem needs to exists beforehand |
|
* @param plot object plot element |
|
*/ |
|
setPlotAttributes: function(plot) { |
|
if (plot.options.type === "square") { |
|
plot.options.attrs.width = plot.options.size; |
|
plot.options.attrs.height = plot.options.size; |
|
plot.options.attrs.x = plot.coords.x - (plot.options.size / 2); |
|
plot.options.attrs.y = plot.coords.y - (plot.options.size / 2); |
|
} else if (plot.options.type === "image") { |
|
plot.options.attrs.src = plot.options.url; |
|
plot.options.attrs.width = plot.options.width; |
|
plot.options.attrs.height = plot.options.height; |
|
plot.options.attrs.x = plot.coords.x - (plot.options.width / 2); |
|
plot.options.attrs.y = plot.coords.y - (plot.options.height / 2); |
|
} else if (plot.options.type === "svg") { |
|
plot.options.attrs.path = plot.options.path; |
|
|
|
// Init transform string |
|
if (plot.options.attrs.transform === undefined) { |
|
plot.options.attrs.transform = ""; |
|
} |
|
|
|
// Retrieve original boundary box if not defined |
|
if (plot.mapElem.originalBBox === undefined) { |
|
plot.mapElem.originalBBox = plot.mapElem.getBBox(); |
|
} |
|
|
|
// The base transform will resize the SVG path to the one specified by width/height |
|
// and also move the path to the actual coordinates |
|
plot.mapElem.baseTransform = "m" + (plot.options.width / plot.mapElem.originalBBox.width) + ",0,0," + |
|
(plot.options.height / plot.mapElem.originalBBox.height) + "," + |
|
(plot.coords.x - plot.options.width / 2) + "," + |
|
(plot.coords.y - plot.options.height / 2); |
|
|
|
plot.options.attrs.transform = plot.mapElem.baseTransform + plot.options.attrs.transform; |
|
|
|
} else { // Default : circle |
|
plot.options.attrs.x = plot.coords.x; |
|
plot.options.attrs.y = plot.coords.y; |
|
plot.options.attrs.r = plot.options.size / 2; |
|
} |
|
}, |
|
|
|
/* |
|
* Draw all links between plots on the paper |
|
*/ |
|
drawLinksCollection: function (linksCollection) { |
|
var self = this; |
|
var p1 = {}; |
|
var p2 = {}; |
|
var coordsP1 = {}; |
|
var coordsP2 = {}; |
|
var links = {}; |
|
|
|
$.each(linksCollection, function (id) { |
|
var elemOptions = self.getElemOptions(self.options.map.defaultLink, linksCollection[id], {}); |
|
|
|
if (typeof linksCollection[id].between[0] === 'string') { |
|
p1 = self.options.plots[linksCollection[id].between[0]]; |
|
} else { |
|
p1 = linksCollection[id].between[0]; |
|
} |
|
|
|
if (typeof linksCollection[id].between[1] === 'string') { |
|
p2 = self.options.plots[linksCollection[id].between[1]]; |
|
} else { |
|
p2 = linksCollection[id].between[1]; |
|
} |
|
|
|
if (p1.plotsOn !== undefined && self.areas[p1.plotsOn] !== undefined) { |
|
var p1BBox = self.areas[p1.plotsOn].mapElem.getBBox(); |
|
coordsP1 = { |
|
x: p1BBox.cx, |
|
y: p1BBox.cy |
|
}; |
|
} |
|
else if (p1.latitude !== undefined && p1.longitude !== undefined) { |
|
coordsP1 = self.mapConf.getCoords(p1.latitude, p1.longitude); |
|
} else { |
|
coordsP1.x = p1.x; |
|
coordsP1.y = p1.y; |
|
} |
|
|
|
if (p2.plotsOn !== undefined && self.areas[p2.plotsOn] !== undefined) { |
|
var p2BBox = self.areas[p2.plotsOn].mapElem.getBBox(); |
|
coordsP2 = { |
|
x: p2BBox.cx, |
|
y: p2BBox.cy |
|
}; |
|
} |
|
else if (p2.latitude !== undefined && p2.longitude !== undefined) { |
|
coordsP2 = self.mapConf.getCoords(p2.latitude, p2.longitude); |
|
} else { |
|
coordsP2.x = p2.x; |
|
coordsP2.y = p2.y; |
|
} |
|
links[id] = self.drawLink(id, coordsP1.x, coordsP1.y, coordsP2.x, coordsP2.y, elemOptions); |
|
}); |
|
return links; |
|
}, |
|
|
|
/* |
|
* Draw a curved link between two couples of coordinates a(xa,ya) and b(xb, yb) on the paper |
|
*/ |
|
drawLink: function (id, xa, ya, xb, yb, elemOptions) { |
|
var self = this; |
|
var link = { |
|
options: elemOptions |
|
}; |
|
// Compute the "curveto" SVG point, d(x,y) |
|
// c(xc, yc) is the center of (xa,ya) and (xb, yb) |
|
var xc = (xa + xb) / 2; |
|
var yc = (ya + yb) / 2; |
|
|
|
// Equation for (cd) : y = acd * x + bcd (d is the cure point) |
|
var acd = -1 / ((yb - ya) / (xb - xa)); |
|
var bcd = yc - acd * xc; |
|
|
|
// dist(c,d) = dist(a,b) (=abDist) |
|
var abDist = Math.sqrt((xb - xa) * (xb - xa) + (yb - ya) * (yb - ya)); |
|
|
|
// Solution for equation dist(cd) = sqrt((xd - xc)² + (yd - yc)²) |
|
// dist(c,d)² = (xd - xc)² + (yd - yc)² |
|
// We assume that dist(c,d) = dist(a,b) |
|
// so : (xd - xc)² + (yd - yc)² - dist(a,b)² = 0 |
|
// With the factor : (xd - xc)² + (yd - yc)² - (factor*dist(a,b))² = 0 |
|
// (xd - xc)² + (acd*xd + bcd - yc)² - (factor*dist(a,b))² = 0 |
|
var a = 1 + acd * acd; |
|
var b = -2 * xc + 2 * acd * bcd - 2 * acd * yc; |
|
var c = xc * xc + bcd * bcd - bcd * yc - yc * bcd + yc * yc - ((elemOptions.factor * abDist) * (elemOptions.factor * abDist)); |
|
var delta = b * b - 4 * a * c; |
|
var x = 0; |
|
var y = 0; |
|
|
|
// There are two solutions, we choose one or the other depending on the sign of the factor |
|
if (elemOptions.factor > 0) { |
|
x = (-b + Math.sqrt(delta)) / (2 * a); |
|
y = acd * x + bcd; |
|
} else { |
|
x = (-b - Math.sqrt(delta)) / (2 * a); |
|
y = acd * x + bcd; |
|
} |
|
|
|
link.mapElem = self.paper.path("m " + xa + "," + ya + " C " + x + "," + y + " " + xb + "," + yb + " " + xb + "," + yb + ""); |
|
|
|
self.initElem(id, 'link', link); |
|
|
|
return link; |
|
}, |
|
|
|
/* |
|
* Check wether newAttrs object bring modifications to originalAttrs object |
|
*/ |
|
isAttrsChanged: function(originalAttrs, newAttrs) { |
|
for (var key in newAttrs) { |
|
if (newAttrs.hasOwnProperty(key) && typeof originalAttrs[key] === 'undefined' || newAttrs[key] !== originalAttrs[key]) { |
|
return true; |
|
} |
|
} |
|
return false; |
|
}, |
|
|
|
/* |
|
* Update the element "elem" on the map with the new options |
|
*/ |
|
updateElem: function (elem, animDuration) { |
|
var self = this; |
|
var mapElemBBox; |
|
var plotOffsetX; |
|
var plotOffsetY; |
|
|
|
if (elem.options.toFront === true) { |
|
elem.mapElem.toFront(); |
|
} |
|
|
|
// Set the cursor attribute related to the HTML link |
|
if (elem.options.href !== undefined) { |
|
elem.options.attrs.cursor = "pointer"; |
|
if (elem.options.text) elem.options.text.attrs.cursor = "pointer"; |
|
} else { |
|
// No HTML links, check if a cursor was defined to pointer |
|
if (elem.mapElem.attrs.cursor === 'pointer') { |
|
elem.options.attrs.cursor = "auto"; |
|
if (elem.options.text) elem.options.text.attrs.cursor = "auto"; |
|
} |
|
} |
|
|
|
// Update the label |
|
if (elem.textElem) { |
|
// Update text attr |
|
elem.options.text.attrs.text = elem.options.text.content; |
|
|
|
// Get mapElem size, and apply an offset to handle future width/height change |
|
mapElemBBox = elem.mapElem.getBBox(); |
|
if (elem.options.size || (elem.options.width && elem.options.height)) { |
|
if (elem.options.type === "image" || elem.options.type === "svg") { |
|
plotOffsetX = (elem.options.width - mapElemBBox.width) / 2; |
|
plotOffsetY = (elem.options.height - mapElemBBox.height) / 2; |
|
} else { |
|
plotOffsetX = (elem.options.size - mapElemBBox.width) / 2; |
|
plotOffsetY = (elem.options.size - mapElemBBox.height) / 2; |
|
} |
|
mapElemBBox.x -= plotOffsetX; |
|
mapElemBBox.x2 += plotOffsetX; |
|
mapElemBBox.y -= plotOffsetY; |
|
mapElemBBox.y2 += plotOffsetY; |
|
} |
|
|
|
// Update position attr |
|
var textPosition = self.getTextPosition(mapElemBBox, elem.options.text.position, elem.options.text.margin); |
|
elem.options.text.attrs.x = textPosition.x; |
|
elem.options.text.attrs.y = textPosition.y; |
|
elem.options.text.attrs['text-anchor'] = textPosition.textAnchor; |
|
|
|
// Update text element attrs and attrsHover |
|
self.setHoverOptions(elem.textElem, elem.options.text.attrs, elem.options.text.attrsHover); |
|
|
|
if (self.isAttrsChanged(elem.textElem.attrs, elem.options.text.attrs)) { |
|
self.animate(elem.textElem, elem.options.text.attrs, animDuration); |
|
} |
|
} |
|
|
|
// Update elements attrs and attrsHover |
|
self.setHoverOptions(elem.mapElem, elem.options.attrs, elem.options.attrsHover); |
|
|
|
if (self.isAttrsChanged(elem.mapElem.attrs, elem.options.attrs)) { |
|
self.animate(elem.mapElem, elem.options.attrs, animDuration); |
|
} |
|
|
|
// Update the cssClass |
|
if (elem.options.cssClass !== undefined) { |
|
$(elem.mapElem.node).removeClass().addClass(elem.options.cssClass); |
|
} |
|
}, |
|
|
|
/* |
|
* Draw the plot |
|
*/ |
|
drawPlot: function (id) { |
|
var self = this; |
|
var plot = {}; |
|
|
|
// Get plot options and store it |
|
plot.options = self.getElemOptions( |
|
self.options.map.defaultPlot, |
|
(self.options.plots[id] ? self.options.plots[id] : {}), |
|
self.options.legend.plot |
|
); |
|
|
|
// Set plot coords |
|
self.setPlotCoords(plot); |
|
|
|
// Draw SVG before setPlotAttributes() |
|
if (plot.options.type === "svg") { |
|
plot.mapElem = self.paper.path(plot.options.path); |
|
} |
|
|
|
// Set plot size attrs |
|
self.setPlotAttributes(plot); |
|
|
|
// Draw other types of plots |
|
if (plot.options.type === "square") { |
|
plot.mapElem = self.paper.rect( |
|
plot.options.attrs.x, |
|
plot.options.attrs.y, |
|
plot.options.attrs.width, |
|
plot.options.attrs.height |
|
); |
|
} else if (plot.options.type === "image") { |
|
plot.mapElem = self.paper.image( |
|
plot.options.attrs.src, |
|
plot.options.attrs.x, |
|
plot.options.attrs.y, |
|
plot.options.attrs.width, |
|
plot.options.attrs.height |
|
); |
|
} else if (plot.options.type === "svg") { |
|
// Nothing to do |
|
} else { |
|
// Default = circle |
|
plot.mapElem = self.paper.circle( |
|
plot.options.attrs.x, |
|
plot.options.attrs.y, |
|
plot.options.attrs.r |
|
); |
|
} |
|
|
|
self.initElem(id, 'plot', plot); |
|
|
|
return plot; |
|
}, |
|
|
|
/* |
|
* Set user defined handlers for events on areas and plots |
|
* @param id the id of the element |
|
* @param type the type of the element (area, plot, link) |
|
* @param elem the element object {mapElem, textElem, options, ...} |
|
*/ |
|
setEventHandlers: function (id, type, elem) { |
|
var self = this; |
|
$.each(elem.options.eventHandlers, function (event) { |
|
if (self.customEventHandlers[event] === undefined) self.customEventHandlers[event] = {}; |
|
if (self.customEventHandlers[event][type] === undefined) self.customEventHandlers[event][type] = {}; |
|
self.customEventHandlers[event][type][id] = elem; |
|
}); |
|
}, |
|
|
|
/* |
|
* Draw a legend for areas and / or plots |
|
* @param legendOptions options for the legend to draw |
|
* @param legendType the type of the legend : "area" or "plot" |
|
* @param elems collection of plots or areas on the maps |
|
* @param legendIndex index of the legend in the conf array |
|
*/ |
|
drawLegend: function (legendOptions, legendType, elems, scale, legendIndex) { |
|
var self = this; |
|
var $legend = {}; |
|
var legendPaper = {}; |
|
var width = 0; |
|
var height = 0; |
|
var title = null; |
|
var titleBBox = null; |
|
var legendElems = {}; |
|
var i = 0; |
|
var x = 0; |
|
var y = 0; |
|
var yCenter = 0; |
|
var sliceOptions = []; |
|
|
|
$legend = $("." + legendOptions.cssClass, self.$container); |
|
|
|
// Save content for later |
|
var initialHTMLContent = $legend.html(); |
|
$legend.empty(); |
|
|
|
legendPaper = new Raphael($legend.get(0)); |
|
// Set some data to object |
|
$(legendPaper.canvas).attr({"data-legend-type": legendType, "data-legend-id": legendIndex}); |
|
|
|
height = width = 0; |
|
|
|
// Set the title of the legend |
|
if (legendOptions.title && legendOptions.title !== "") { |
|
title = legendPaper.text(legendOptions.marginLeftTitle, 0, legendOptions.title).attr(legendOptions.titleAttrs); |
|
titleBBox = title.getBBox(); |
|
title.attr({y: 0.5 * titleBBox.height}); |
|
|
|
width = legendOptions.marginLeftTitle + titleBBox.width; |
|
height += legendOptions.marginBottomTitle + titleBBox.height; |
|
} |
|
|
|
// Calculate attrs (and width, height and r (radius)) for legend elements, and yCenter for horizontal legends |
|
|
|
for (i = 0; i < legendOptions.slices.length; ++i) { |
|
var yCenterCurrent = 0; |
|
|
|
sliceOptions[i] = $.extend(true, {}, (legendType === "plot") ? self.options.map.defaultPlot : self.options.map.defaultArea, legendOptions.slices[i]); |
|
|
|
if (legendOptions.slices[i].legendSpecificAttrs === undefined) { |
|
legendOptions.slices[i].legendSpecificAttrs = {}; |
|
} |
|
|
|
$.extend(true, sliceOptions[i].attrs, legendOptions.slices[i].legendSpecificAttrs); |
|
|
|
if (legendType === "area") { |
|
if (sliceOptions[i].attrs.width === undefined) |
|
sliceOptions[i].attrs.width = 30; |
|
if (sliceOptions[i].attrs.height === undefined) |
|
sliceOptions[i].attrs.height = 20; |
|
} else if (sliceOptions[i].type === "square") { |
|
if (sliceOptions[i].attrs.width === undefined) |
|
sliceOptions[i].attrs.width = sliceOptions[i].size; |
|
if (sliceOptions[i].attrs.height === undefined) |
|
sliceOptions[i].attrs.height = sliceOptions[i].size; |
|
} else if (sliceOptions[i].type === "image" || sliceOptions[i].type === "svg") { |
|
if (sliceOptions[i].attrs.width === undefined) |
|
sliceOptions[i].attrs.width = sliceOptions[i].width; |
|
if (sliceOptions[i].attrs.height === undefined) |
|
sliceOptions[i].attrs.height = sliceOptions[i].height; |
|
} else { |
|
if (sliceOptions[i].attrs.r === undefined) |
|
sliceOptions[i].attrs.r = sliceOptions[i].size / 2; |
|
} |
|
|
|
// Compute yCenter for this legend slice |
|
yCenterCurrent = legendOptions.marginBottomTitle; |
|
// Add title height if it exists |
|
if (title) { |
|
yCenterCurrent += titleBBox.height; |
|
} |
|
if (legendType === "plot" && (sliceOptions[i].type === undefined || sliceOptions[i].type === "circle")) { |
|
yCenterCurrent += scale * sliceOptions[i].attrs.r; |
|
} else { |
|
yCenterCurrent += scale * sliceOptions[i].attrs.height / 2; |
|
} |
|
// Update yCenter if current larger |
|
yCenter = Math.max(yCenter, yCenterCurrent); |
|
} |
|
|
|
if (legendOptions.mode === "horizontal") { |
|
width = legendOptions.marginLeft; |
|
} |
|
|
|
// Draw legend elements (circle, square or image in vertical or horizontal mode) |
|
for (i = 0; i < sliceOptions.length; ++i) { |
|
var legendElem = {}; |
|
var legendElemBBox = {}; |
|
var legendLabel = {}; |
|
|
|
if (sliceOptions[i].display === undefined || sliceOptions[i].display === true) { |
|
if (legendType === "area") { |
|
if (legendOptions.mode === "horizontal") { |
|
x = width + legendOptions.marginLeft; |
|
y = yCenter - (0.5 * scale * sliceOptions[i].attrs.height); |
|
} else { |
|
x = legendOptions.marginLeft; |
|
y = height; |
|
} |
|
|
|
legendElem = legendPaper.rect(x, y, scale * (sliceOptions[i].attrs.width), scale * (sliceOptions[i].attrs.height)); |
|
} else if (sliceOptions[i].type === "square") { |
|
if (legendOptions.mode === "horizontal") { |
|
x = width + legendOptions.marginLeft; |
|
y = yCenter - (0.5 * scale * sliceOptions[i].attrs.height); |
|
} else { |
|
x = legendOptions.marginLeft; |
|
y = height; |
|
} |
|
|
|
legendElem = legendPaper.rect(x, y, scale * (sliceOptions[i].attrs.width), scale * (sliceOptions[i].attrs.height)); |
|
|
|
} else if (sliceOptions[i].type === "image" || sliceOptions[i].type === "svg") { |
|
if (legendOptions.mode === "horizontal") { |
|
x = width + legendOptions.marginLeft; |
|
y = yCenter - (0.5 * scale * sliceOptions[i].attrs.height); |
|
} else { |
|
x = legendOptions.marginLeft; |
|
y = height; |
|
} |
|
|
|
if (sliceOptions[i].type === "image") { |
|
legendElem = legendPaper.image( |
|
sliceOptions[i].url, x, y, scale * sliceOptions[i].attrs.width, scale * sliceOptions[i].attrs.height); |
|
} else { |
|
legendElem = legendPaper.path(sliceOptions[i].path); |
|
|
|
if (sliceOptions[i].attrs.transform === undefined) { |
|
sliceOptions[i].attrs.transform = ""; |
|
} |
|
legendElemBBox = legendElem.getBBox(); |
|
sliceOptions[i].attrs.transform = "m" + ((scale * sliceOptions[i].width) / legendElemBBox.width) + ",0,0," + ((scale * sliceOptions[i].height) / legendElemBBox.height) + "," + x + "," + y + sliceOptions[i].attrs.transform; |
|
} |
|
} else { |
|
if (legendOptions.mode === "horizontal") { |
|
x = width + legendOptions.marginLeft + scale * (sliceOptions[i].attrs.r); |
|
y = yCenter; |
|
} else { |
|
x = legendOptions.marginLeft + scale * (sliceOptions[i].attrs.r); |
|
y = height + scale * (sliceOptions[i].attrs.r); |
|
} |
|
legendElem = legendPaper.circle(x, y, scale * (sliceOptions[i].attrs.r)); |
|
} |
|
|
|
// Set attrs to the element drawn above |
|
delete sliceOptions[i].attrs.width; |
|
delete sliceOptions[i].attrs.height; |
|
delete sliceOptions[i].attrs.r; |
|
legendElem.attr(sliceOptions[i].attrs); |
|
legendElemBBox = legendElem.getBBox(); |
|
|
|
// Draw the label associated with the element |
|
if (legendOptions.mode === "horizontal") { |
|
x = width + legendOptions.marginLeft + legendElemBBox.width + legendOptions.marginLeftLabel; |
|
y = yCenter; |
|
} else { |
|
x = legendOptions.marginLeft + legendElemBBox.width + legendOptions.marginLeftLabel; |
|
y = height + (legendElemBBox.height / 2); |
|
} |
|
|
|
legendLabel = legendPaper.text(x, y, sliceOptions[i].label).attr(legendOptions.labelAttrs); |
|
|
|
// Update the width and height for the paper |
|
if (legendOptions.mode === "horizontal") { |
|
var currentHeight = legendOptions.marginBottom + legendElemBBox.height; |
|
width += legendOptions.marginLeft + legendElemBBox.width + legendOptions.marginLeftLabel + legendLabel.getBBox().width; |
|
if (sliceOptions[i].type !== "image" && legendType !== "area") { |
|
currentHeight += legendOptions.marginBottomTitle; |
|
} |
|
// Add title height if it exists |
|
if (title) { |
|
currentHeight += titleBBox.height; |
|
} |
|
height = Math.max(height, currentHeight); |
|
} else { |
|
width = Math.max(width, legendOptions.marginLeft + legendElemBBox.width + legendOptions.marginLeftLabel + legendLabel.getBBox().width); |
|
height += legendOptions.marginBottom + legendElemBBox.height; |
|
} |
|
|
|
// Set some data to elements |
|
$(legendElem.node).attr({ |
|
"data-legend-id": legendIndex, |
|
"data-legend-type": legendType, |
|
"data-type": "legend-elem", |
|
"data-id": i, |
|
"data-hidden": 0 |
|
}); |
|
$(legendLabel.node).attr({ |
|
"data-legend-id": legendIndex, |
|
"data-legend-type": legendType, |
|
"data-type": "legend-label", |
|
"data-id": i, |
|
"data-hidden": 0 |
|
}); |
|
|
|
// Set array content |
|
// We use similar names like map/plots/links |
|
legendElems[i] = { |
|
mapElem: legendElem, |
|
textElem: legendLabel |
|
}; |
|
|
|
// Hide map elements when the user clicks on a legend item |
|
if (legendOptions.hideElemsOnClick.enabled) { |
|
// Hide/show elements when user clicks on a legend element |
|
legendLabel.attr({cursor: "pointer"}); |
|
legendElem.attr({cursor: "pointer"}); |
|
|
|
self.setHoverOptions(legendElem, sliceOptions[i].attrs, sliceOptions[i].attrs); |
|
self.setHoverOptions(legendLabel, legendOptions.labelAttrs, legendOptions.labelAttrsHover); |
|
|
|
if (sliceOptions[i].clicked !== undefined && sliceOptions[i].clicked === true) { |
|
self.handleClickOnLegendElem(legendElems[i], i, legendIndex, legendType, {hideOtherElems: false}); |
|
} |
|
} |
|
} |
|
} |
|
|
|
// VMLWidth option allows you to set static width for the legend |
|
// only for VML render because text.getBBox() returns wrong values on IE6/7 |
|
if (Raphael.type !== "SVG" && legendOptions.VMLWidth) |
|
width = legendOptions.VMLWidth; |
|
|
|
legendPaper.setSize(width, height); |
|
|
|
return { |
|
container: $legend, |
|
initialHTMLContent: initialHTMLContent, |
|
elems: legendElems |
|
}; |
|
}, |
|
|
|
/* |
|
* Allow to hide elements of the map when the user clicks on a related legend item |
|
* @param elem legend element |
|
* @param id legend element ID |
|
* @param legendIndex corresponding legend index |
|
* @param legendType corresponding legend type (area or plot) |
|
* @param opts object additionnal options |
|
* hideOtherElems boolean, if other elems shall be hidden |
|
* animDuration duration of animation |
|
*/ |
|
handleClickOnLegendElem: function(elem, id, legendIndex, legendType, opts) { |
|
var self = this; |
|
var legendOptions; |
|
opts = opts || {}; |
|
|
|
if (!$.isArray(self.options.legend[legendType])) { |
|
legendOptions = self.options.legend[legendType]; |
|
} else { |
|
legendOptions = self.options.legend[legendType][legendIndex]; |
|
} |
|
|
|
var legendElem = elem.mapElem; |
|
var legendLabel = elem.textElem; |
|
var $legendElem = $(legendElem.node); |
|
var $legendLabel = $(legendLabel.node); |
|
var sliceOptions = legendOptions.slices[id]; |
|
var mapElems = legendType === 'area' ? self.areas : self.plots; |
|
// Check animDuration: if not set, this is a regular click, use the value specified in options |
|
var animDuration = opts.animDuration !== undefined ? opts.animDuration : legendOptions.hideElemsOnClick.animDuration ; |
|
|
|
var hidden = $legendElem.attr('data-hidden'); |
|
var hiddenNewAttr = (hidden === '0') ? {"data-hidden": '1'} : {"data-hidden": '0'}; |
|
|
|
if (hidden === '0') { |
|
self.animate(legendLabel, {"opacity": 0.5}, animDuration); |
|
} else { |
|
self.animate(legendLabel, {"opacity": 1}, animDuration); |
|
} |
|
|
|
$.each(mapElems, function (y) { |
|
var elemValue; |
|
|
|
// Retreive stored data of element |
|
// 'hidden-by' contains the list of legendIndex that is hiding this element |
|
var hiddenBy = mapElems[y].mapElem.data('hidden-by'); |
|
// Set to empty object if undefined |
|
if (hiddenBy === undefined) hiddenBy = {}; |
|
|
|
if ($.isArray(mapElems[y].options.value)) { |
|
elemValue = mapElems[y].options.value[legendIndex]; |
|
} else { |
|
elemValue = mapElems[y].options.value; |
|
} |
|
|
|
// Hide elements whose value matches with the slice of the clicked legend item |
|
if (self.getLegendSlice(elemValue, legendOptions) === sliceOptions) { |
|
if (hidden === '0') { // we want to hide this element |
|
hiddenBy[legendIndex] = true; // add legendIndex to the data object for later use |
|
self.setElementOpacity(mapElems[y], legendOptions.hideElemsOnClick.opacity, animDuration); |
|
} else { // We want to show this element |
|
delete hiddenBy[legendIndex]; // Remove this legendIndex from object |
|
// Check if another legendIndex is defined |
|
// We will show this element only if no legend is no longer hiding it |
|
if ($.isEmptyObject(hiddenBy)) { |
|
self.setElementOpacity( |
|
mapElems[y], |
|
mapElems[y].mapElem.originalAttrs.opacity !== undefined ? mapElems[y].mapElem.originalAttrs.opacity : 1, |
|
animDuration |
|
); |
|
} |
|
} |
|
// Update elem data with new values |
|
mapElems[y].mapElem.data('hidden-by', hiddenBy); |
|
} |
|
}); |
|
|
|
$legendElem.attr(hiddenNewAttr); |
|
$legendLabel.attr(hiddenNewAttr); |
|
|
|
if ((opts.hideOtherElems === undefined || opts.hideOtherElems === true) && legendOptions.exclusive === true ) { |
|
$("[data-type='legend-elem'][data-hidden=0]", self.$container).each(function () { |
|
var $elem = $(this); |
|
if ($elem.attr('data-id') !== id) { |
|
$elem.trigger("click", {hideOtherElems: false}); |
|
} |
|
}); |
|
} |
|
|
|
}, |
|
|
|
/* |
|
* Create all legends for a specified type (area or plot) |
|
* @param legendType the type of the legend : "area" or "plot" |
|
* @param elems collection of plots or areas displayed on the map |
|
* @param scale scale ratio of the map |
|
*/ |
|
createLegends: function (legendType, elems, scale) { |
|
var self = this; |
|
var legendsOptions = self.options.legend[legendType]; |
|
|
|
if (!$.isArray(self.options.legend[legendType])) { |
|
legendsOptions = [self.options.legend[legendType]]; |
|
} |
|
|
|
self.legends[legendType] = {}; |
|
for (var j = 0; j < legendsOptions.length; ++j) { |
|
if (legendsOptions[j].display === true && $.isArray(legendsOptions[j].slices) && legendsOptions[j].slices.length > 0 && |
|
legendsOptions[j].cssClass !== "" && $("." + legendsOptions[j].cssClass, self.$container).length !== 0 |
|
) { |
|
self.legends[legendType][j] = self.drawLegend(legendsOptions[j], legendType, elems, scale, j); |
|
} |
|
} |
|
}, |
|
|
|
/* |
|
* Set the attributes on hover and the attributes to restore for a map element |
|
* @param elem the map element |
|
* @param originalAttrs the original attributes to restore on mouseout event |
|
* @param attrsHover the attributes to set on mouseover event |
|
*/ |
|
setHoverOptions: function (elem, originalAttrs, attrsHover) { |
|
// Disable transform option on hover for VML (IE<9) because of several bugs |
|
if (Raphael.type !== "SVG") delete attrsHover.transform; |
|
elem.attrsHover = attrsHover; |
|
|
|
if (elem.attrsHover.transform) elem.originalAttrs = $.extend({transform: "s1"}, originalAttrs); |
|
else elem.originalAttrs = originalAttrs; |
|
}, |
|
|
|
/* |
|
* Set the behaviour when mouse enters element ("mouseover" event) |
|
* It may be an area, a plot, a link or a legend element |
|
* @param elem the map element |
|
*/ |
|
elemEnter: function (elem) { |
|
var self = this; |
|
if (elem === undefined) return; |
|
|
|
/* Handle mapElem Hover attributes */ |
|
if (elem.mapElem !== undefined) { |
|
self.animate(elem.mapElem, elem.mapElem.attrsHover, elem.mapElem.attrsHover.animDuration); |
|
} |
|
|
|
/* Handle textElem Hover attributes */ |
|
if (elem.textElem !== undefined) { |
|
self.animate(elem.textElem, elem.textElem.attrsHover, elem.textElem.attrsHover.animDuration); |
|
} |
|
|
|
/* Handle tooltip init */ |
|
if (elem.options && elem.options.tooltip !== undefined) { |
|
var content = ''; |
|
// Reset classes |
|
self.$tooltip.removeClass().addClass(self.options.map.tooltip.cssClass); |
|
// Get content |
|
if (elem.options.tooltip.content !== undefined) { |
|
// if tooltip.content is function, call it. Otherwise, assign it directly. |
|
if (typeof elem.options.tooltip.content === "function") content = elem.options.tooltip.content(elem.mapElem); |
|
else content = elem.options.tooltip.content; |
|
} |
|
if (elem.options.tooltip.cssClass !== undefined) { |
|
self.$tooltip.addClass(elem.options.tooltip.cssClass); |
|
} |
|
self.$tooltip.html(content).css("display", "block"); |
|
} |
|
|
|
// workaround for older version of Raphael |
|
if (elem.mapElem !== undefined || elem.textElem !== undefined) { |
|
if (self.paper.safari) self.paper.safari(); |
|
} |
|
}, |
|
|
|
/* |
|
* Set the behaviour when mouse moves in element ("mousemove" event) |
|
* @param elem the map element |
|
*/ |
|
elemHover: function (elem, event) { |
|
var self = this; |
|
if (elem === undefined) return; |
|
|
|
/* Handle tooltip position update */ |
|
if (elem.options.tooltip !== undefined) { |
|
var mouseX = event.pageX; |
|
var mouseY = event.pageY; |
|
|
|
var offsetLeft = 10; |
|
var offsetTop = 20; |
|
if (typeof elem.options.tooltip.offset === "object") { |
|
if (typeof elem.options.tooltip.offset.left !== "undefined") { |
|
offsetLeft = elem.options.tooltip.offset.left; |
|
} |
|
if (typeof elem.options.tooltip.offset.top !== "undefined") { |
|
offsetTop = elem.options.tooltip.offset.top; |
|
} |
|
} |
|
|
|
var tooltipPosition = { |
|
"left": Math.min(self.$map.width() - self.$tooltip.outerWidth() - 5, |
|
mouseX - self.$map.offset().left + offsetLeft), |
|
"top": Math.min(self.$map.height() - self.$tooltip.outerHeight() - 5, |
|
mouseY - self.$map.offset().top + offsetTop) |
|
}; |
|
|
|
if (typeof elem.options.tooltip.overflow === "object") { |
|
if (elem.options.tooltip.overflow.right === true) { |
|
tooltipPosition.left = mouseX - self.$map.offset().left + 10; |
|
} |
|
if (elem.options.tooltip.overflow.bottom === true) { |
|
tooltipPosition.top = mouseY - self.$map.offset().top + 20; |
|
} |
|
} |
|
|
|
self.$tooltip.css(tooltipPosition); |
|
} |
|
}, |
|
|
|
/* |
|
* Set the behaviour when mouse leaves element ("mouseout" event) |
|
* It may be an area, a plot, a link or a legend element |
|
* @param elem the map element |
|
*/ |
|
elemOut: function (elem) { |
|
var self = this; |
|
if (elem === undefined) return; |
|
|
|
/* reset mapElem attributes */ |
|
if (elem.mapElem !== undefined) { |
|
self.animate(elem.mapElem, elem.mapElem.originalAttrs, elem.mapElem.attrsHover.animDuration); |
|
} |
|
|
|
/* reset textElem attributes */ |
|
if (elem.textElem !== undefined) { |
|
self.animate(elem.textElem, elem.textElem.originalAttrs, elem.textElem.attrsHover.animDuration); |
|
} |
|
|
|
/* reset tooltip */ |
|
if (elem.options && elem.options.tooltip !== undefined) { |
|
self.$tooltip.css({ |
|
'display': 'none', |
|
'top': -1000, |
|
'left': -1000 |
|
}); |
|
} |
|
|
|
// workaround for older version of Raphael |
|
if (elem.mapElem !== undefined || elem.textElem !== undefined) { |
|
if (self.paper.safari) self.paper.safari(); |
|
} |
|
}, |
|
|
|
/* |
|
* Set the behaviour when mouse clicks element ("click" event) |
|
* It may be an area, a plot or a link (but not a legend element which has its own function) |
|
* @param elem the map element |
|
*/ |
|
elemClick: function (elem) { |
|
var self = this; |
|
if (elem === undefined) return; |
|
|
|
/* Handle click when href defined */ |
|
if (!self.panning && elem.options.href !== undefined) { |
|
window.open(elem.options.href, elem.options.target); |
|
} |
|
}, |
|
|
|
/* |
|
* Get element options by merging default options, element options and legend options |
|
* @param defaultOptions |
|
* @param elemOptions |
|
* @param legendOptions |
|
*/ |
|
getElemOptions: function (defaultOptions, elemOptions, legendOptions) { |
|
var self = this; |
|
var options = $.extend(true, {}, defaultOptions, elemOptions); |
|
if (options.value !== undefined) { |
|
if ($.isArray(legendOptions)) { |
|
for (var i = 0; i < legendOptions.length; ++i) { |
|
options = $.extend(true, {}, options, self.getLegendSlice(options.value[i], legendOptions[i])); |
|
} |
|
} else { |
|
options = $.extend(true, {}, options, self.getLegendSlice(options.value, legendOptions)); |
|
} |
|
} |
|
return options; |
|
}, |
|
|
|
/* |
|
* Get the coordinates of the text relative to a bbox and a position |
|
* @param bbox the boundary box of the element |
|
* @param textPosition the wanted text position (inner, right, left, top or bottom) |
|
* @param margin number or object {x: val, y:val} margin between the bbox and the text |
|
*/ |
|
getTextPosition: function (bbox, textPosition, margin) { |
|
var textX = 0; |
|
var textY = 0; |
|
var textAnchor = ""; |
|
|
|
if (typeof margin === "number") { |
|
if (textPosition === "bottom" || textPosition === "top") { |
|
margin = {x: 0, y: margin}; |
|
} else if (textPosition === "right" || textPosition === "left") { |
|
margin = {x: margin, y: 0}; |
|
} else { |
|
margin = {x: 0, y: 0}; |
|
} |
|
} |
|
|
|
switch (textPosition) { |
|
case "bottom" : |
|
textX = ((bbox.x + bbox.x2) / 2) + margin.x; |
|
textY = bbox.y2 + margin.y; |
|
textAnchor = "middle"; |
|
break; |
|
case "top" : |
|
textX = ((bbox.x + bbox.x2) / 2) + margin.x; |
|
textY = bbox.y - margin.y; |
|
textAnchor = "middle"; |
|
break; |
|
case "left" : |
|
textX = bbox.x - margin.x; |
|
textY = ((bbox.y + bbox.y2) / 2) + margin.y; |
|
textAnchor = "end"; |
|
break; |
|
case "right" : |
|
textX = bbox.x2 + margin.x; |
|
textY = ((bbox.y + bbox.y2) / 2) + margin.y; |
|
textAnchor = "start"; |
|
break; |
|
default : // "inner" position |
|
textX = ((bbox.x + bbox.x2) / 2) + margin.x; |
|
textY = ((bbox.y + bbox.y2) / 2) + margin.y; |
|
textAnchor = "middle"; |
|
} |
|
return {"x": textX, "y": textY, "textAnchor": textAnchor}; |
|
}, |
|
|
|
/* |
|
* Get the legend conf matching with the value |
|
* @param value the value to match with a slice in the legend |
|
* @param legend the legend params object |
|
* @return the legend slice matching with the value |
|
*/ |
|
getLegendSlice: function (value, legend) { |
|
for (var i = 0; i < legend.slices.length; ++i) { |
|
if ((legend.slices[i].sliceValue !== undefined && value === legend.slices[i].sliceValue) || |
|
((legend.slices[i].sliceValue === undefined) && |
|
(legend.slices[i].min === undefined || value >= legend.slices[i].min) && |
|
(legend.slices[i].max === undefined || value <= legend.slices[i].max)) |
|
) { |
|
return legend.slices[i]; |
|
} |
|
} |
|
return {}; |
|
}, |
|
|
|
/* |
|
* Animated view box changes |
|
* As from http://code.voidblossom.com/animating-viewbox-easing-formulas/, |
|
* (from https://github.com/theshaun works on mapael) |
|
* @param x coordinate of the point to focus on |
|
* @param y coordinate of the point to focus on |
|
* @param w map defined width |
|
* @param h map defined height |
|
* @param duration defined length of time for animation |
|
* @param easingFunction defined Raphael supported easing_formula to use |
|
*/ |
|
animateViewBox: function (targetX, targetY, targetW, targetH, duration, easingFunction) { |
|
var self = this; |
|
|
|
var cx = self.currentViewBox.x; |
|
var dx = targetX - cx; |
|
var cy = self.currentViewBox.y; |
|
var dy = targetY - cy; |
|
var cw = self.currentViewBox.w; |
|
var dw = targetW - cw; |
|
var ch = self.currentViewBox.h; |
|
var dh = targetH - ch; |
|
|
|
// Init current ViewBox target if undefined |
|
if (!self.zoomAnimCVBTarget) { |
|
self.zoomAnimCVBTarget = { |
|
x: targetX, y: targetY, w: targetW, h: targetH |
|
}; |
|
} |
|
|
|
// Determine zoom direction by comparig current vs. target width |
|
var zoomDir = (cw > targetW) ? 'in' : 'out'; |
|
|
|
var easingFormula = Raphael.easing_formulas[easingFunction || "linear"]; |
|
|
|
// To avoid another frame when elapsed time approach end (2%) |
|
var durationWithMargin = duration - (duration * 2 / 100); |
|
|
|
// Save current zoomAnimStartTime before assigning a new one |
|
var oldZoomAnimStartTime = self.zoomAnimStartTime; |
|
self.zoomAnimStartTime = (new Date()).getTime(); |
|
|
|
/* Actual function to animate the ViewBox |
|
* Uses requestAnimationFrame to schedule itself again until animation is over |
|
*/ |
|
var computeNextStep = function () { |
|
// Cancel any remaining animationFrame |
|
// It means this new step will take precedence over the old one scheduled |
|
// This is the case when the user is triggering the zoom fast (e.g. with a big mousewheel run) |
|
// This actually does nothing when performing a single zoom action |
|
self.cancelAnimationFrame(self.zoomAnimID); |
|
// Compute elapsed time |
|
var elapsed = (new Date()).getTime() - self.zoomAnimStartTime; |
|
// Check if animation should finish |
|
if (elapsed < durationWithMargin) { |
|
// Hold the future ViewBox values |
|
var x, y, w, h; |
|
|
|
// There are two ways to compute the next ViewBox size |
|
// 1. If the target ViewBox has changed between steps (=> ADAPTATION step) |
|
// 2. Or if the target ViewBox is the same (=> NORMAL step) |
|
// |
|
// A change of ViewBox target between steps means the user is triggering |
|
// the zoom fast (like a big scroll with its mousewheel) |
|
// |
|
// The new animation step with the new target will always take precedence over the |
|
// last one and start from 0 (we overwrite zoomAnimStartTime and cancel the scheduled frame) |
|
// |
|
// So if we don't detect the change of target and adapt our computation, |
|
// the user will see a delay at beginning the ratio will stays at 0 for some frames |
|
// |
|
// Hence when detecting the change of target, we animate from the previous target. |
|
// |
|
// The next step will then take the lead and continue from there, achieving a nicer |
|
// experience for user. |
|
|
|
// Change of target IF: an old animation start value exists AND the target has actually changed |
|
if (oldZoomAnimStartTime && self.zoomAnimCVBTarget && self.zoomAnimCVBTarget.w !== targetW) { |
|
// Compute the real time elapsed with the last step |
|
var realElapsed = (new Date()).getTime() - oldZoomAnimStartTime; |
|
// Compute then the actual ratio we're at |
|
var realRatio = easingFormula(realElapsed / duration); |
|
// Compute new ViewBox values |
|
// The difference with the normal function is regarding the delta value used |
|
// We don't take the current (dx, dy, dw, dh) values yet because they are related to the new target |
|
// But we take the old target |
|
x = cx + (self.zoomAnimCVBTarget.x - cx) * realRatio; |
|
y = cy + (self.zoomAnimCVBTarget.y - cy) * realRatio; |
|
w = cw + (self.zoomAnimCVBTarget.w - cw) * realRatio; |
|
h = ch + (self.zoomAnimCVBTarget.h - ch) * realRatio; |
|
// Update cw, cy, cw and ch so the next step take animation from here |
|
cx = x; |
|
dx = targetX - cx; |
|
cy = y; |
|
dy = targetY - cy; |
|
cw = w; |
|
dw = targetW - cw; |
|
ch = h; |
|
dh = targetH - ch; |
|
// Update the current ViewBox target |
|
self.zoomAnimCVBTarget = { |
|
x: targetX, y: targetY, w: targetW, h: targetH |
|
}; |
|
} else { |
|
// This is the classical approach when nothing come interrupting the zoom |
|
// Compute ratio according to elasped time and easing formula |
|
var ratio = easingFormula(elapsed / duration); |
|
// From the current value, we add a delta with a ratio that will leads us to the target |
|
x = cx + dx * ratio; |
|
y = cy + dy * ratio; |
|
w = cw + dw * ratio; |
|
h = ch + dh * ratio; |
|
} |
|
|
|
// Some checks before applying the new viewBox |
|
if (zoomDir === 'in' && (w > self.currentViewBox.w || w < targetW)) { |
|
// Zooming IN and the new ViewBox seems larger than the current value, or smaller than target value |
|
// We do NOT set the ViewBox with this value |
|
// Otherwise, the user would see the camera going back and forth |
|
} else if (zoomDir === 'out' && (w < self.currentViewBox.w || w > targetW)) { |
|
// Zooming OUT and the new ViewBox seems smaller than the current value, or larger than target value |
|
// We do NOT set the ViewBox with this value |
|
// Otherwise, the user would see the camera going back and forth |
|
} else { |
|
// New values look good, applying |
|
self.setViewBox(x, y, w, h); |
|
} |
|
|
|
// Schedule the next step |
|
self.zoomAnimID = self.requestAnimationFrame(computeNextStep); |
|
} else { |
|
/* Zoom animation done ! */ |
|
// Perform some cleaning |
|
self.zoomAnimStartTime = null; |
|
self.zoomAnimCVBTarget = null; |
|
// Make sure the ViewBox hits the target! |
|
if (self.currentViewBox.w !== targetW) { |
|
self.setViewBox(targetX, targetY, targetW, targetH); |
|
} |
|
// Finally trigger afterZoom event |
|
self.$map.trigger("afterZoom", { |
|
x1: targetX, y1: targetY, |
|
x2: (targetX + targetW), y2: (targetY + targetH) |
|
}); |
|
} |
|
}; |
|
|
|
// Invoke the first step directly |
|
computeNextStep(); |
|
}, |
|
|
|
/* |
|
* requestAnimationFrame/cancelAnimationFrame polyfill |
|
* Based on https://gist.github.com/jlmakes/47eba84c54bc306186ac1ab2ffd336d4 |
|
* and also https://gist.github.com/paulirish/1579671 |
|
* |
|
* _requestAnimationFrameFn and _cancelAnimationFrameFn hold the current functions |
|
* But requestAnimationFrame and cancelAnimationFrame shall be called since |
|
* in order to be in window context |
|
*/ |
|
// The function to use for requestAnimationFrame |
|
requestAnimationFrame: function(callback) { |
|
return this._requestAnimationFrameFn.call(window, callback); |
|
}, |
|
// The function to use for cancelAnimationFrame |
|
cancelAnimationFrame: function(id) { |
|
this._cancelAnimationFrameFn.call(window, id); |
|
}, |
|
// The requestAnimationFrame polyfill'd function |
|
// Value set by self-invoking function, will be run only once |
|
_requestAnimationFrameFn: (function () { |
|
var polyfill = (function () { |
|
var clock = (new Date()).getTime(); |
|
|
|
return function (callback) { |
|
var currentTime = (new Date()).getTime(); |
|
|
|
// requestAnimationFrame strive to run @60FPS |
|
// (e.g. every 16 ms) |
|
if (currentTime - clock > 16) { |
|
clock = currentTime; |
|
callback(currentTime); |
|
} else { |
|
// Ask browser to schedule next callback when possible |
|
return setTimeout(function () { |
|
polyfill(callback); |
|
}, 0); |
|
} |
|
}; |
|
})(); |
|
|
|
return window.requestAnimationFrame || |
|
window.webkitRequestAnimationFrame || |
|
window.mozRequestAnimationFrame || |
|
window.msRequestAnimationFrame || |
|
window.oRequestAnimationFrame || |
|
polyfill; |
|
})(), |
|
// The CancelAnimationFrame polyfill'd function |
|
// Value set by self-invoking function, will be run only once |
|
_cancelAnimationFrameFn: (function () { |
|
return window.cancelAnimationFrame || |
|
window.webkitCancelAnimationFrame || |
|
window.webkitCancelRequestAnimationFrame || |
|
window.mozCancelAnimationFrame || |
|
window.mozCancelRequestAnimationFrame || |
|
window.msCancelAnimationFrame || |
|
window.msCancelRequestAnimationFrame || |
|
window.oCancelAnimationFrame || |
|
window.oCancelRequestAnimationFrame || |
|
clearTimeout; |
|
})(), |
|
|
|
/* |
|
* SetViewBox wrapper |
|
* Apply new viewbox values and keep track of them |
|
* |
|
* This avoid using the internal variable paper._viewBox which |
|
* may not be present in future version of Raphael |
|
*/ |
|
setViewBox: function(x, y, w, h) { |
|
var self = this; |
|
// Update current value |
|
self.currentViewBox.x = x; |
|
self.currentViewBox.y = y; |
|
self.currentViewBox.w = w; |
|
self.currentViewBox.h = h; |
|
// Perform set view box |
|
self.paper.setViewBox(x, y, w, h, false); |
|
}, |
|
|
|
/* |
|
* Animate wrapper for Raphael element |
|
* |
|
* Perform an animation and ensure the non-animated attr are set. |
|
* This is needed for specific attributes like cursor who will not |
|
* be animated, and thus not set. |
|
* |
|
* If duration is set to 0 (or not set), no animation are performed |
|
* and attributes are directly set (and the callback directly called) |
|
*/ |
|
// List extracted from Raphael internal vars |
|
// Diff between Raphael.availableAttrs and Raphael._availableAnimAttrs |
|
_nonAnimatedAttrs: [ |
|
"arrow-end", "arrow-start", "gradient", |
|
"class", "cursor", "text-anchor", |
|
"font", "font-family", "font-style", "font-weight", "letter-spacing", |
|
"src", "href", "target", "title", |
|
"stroke-dasharray", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit" |
|
], |
|
/* |
|
* @param element Raphael element |
|
* @param attrs Attributes object to animate |
|
* @param duration Animation duration in ms |
|
* @param callback Callback to eventually call after animation is done |
|
*/ |
|
animate: function(element, attrs, duration, callback) { |
|
var self = this; |
|
// Check element |
|
if (!element) return; |
|
if (duration > 0) { |
|
// Filter out non-animated attributes |
|
// Note: we don't need to delete from original attribute (they won't be set anyway) |
|
var attrsNonAnimated = {}; |
|
for (var i=0 ; i < self._nonAnimatedAttrs.length ; i++) { |
|
var attrName = self._nonAnimatedAttrs[i]; |
|
if (attrs[attrName] !== undefined) { |
|
attrsNonAnimated[attrName] = attrs[attrName]; |
|
} |
|
} |
|
// Set non-animated attributes |
|
element.attr(attrsNonAnimated); |
|
// Start animation for all attributes |
|
element.animate(attrs, duration, 'linear', function() { |
|
if (callback) callback(); |
|
}); |
|
} else { |
|
// No animation: simply set all attributes... |
|
element.attr(attrs); |
|
// ... and call the callback if needed |
|
if (callback) callback(); |
|
} |
|
}, |
|
|
|
/* |
|
* Check for Raphael bug regarding drawing while beeing hidden (under display:none) |
|
* See https://github.com/neveldo/jQuery-Mapael/issues/135 |
|
* @return true/false |
|
* |
|
* Wants to override this behavior? Use prototype overriding: |
|
* $.mapael.prototype.isRaphaelBBoxBugPresent = function() {return false;}; |
|
*/ |
|
isRaphaelBBoxBugPresent: function() { |
|
var self = this; |
|
// Draw text, then get its boundaries |
|
var textElem = self.paper.text(-50, -50, "TEST"); |
|
var textElemBBox = textElem.getBBox(); |
|
// remove element |
|
textElem.remove(); |
|
// If it has no height and width, then the paper is hidden |
|
return (textElemBBox.width === 0 && textElemBBox.height === 0); |
|
}, |
|
|
|
// Default map options |
|
defaultOptions: { |
|
map: { |
|
cssClass: "map", |
|
tooltip: { |
|
cssClass: "mapTooltip" |
|
}, |
|
defaultArea: { |
|
attrs: { |
|
fill: "#343434", |
|
stroke: "#5d5d5d", |
|
"stroke-width": 1, |
|
"stroke-linejoin": "round" |
|
}, |
|
attrsHover: { |
|
fill: "#f38a03", |
|
animDuration: 300 |
|
}, |
|
text: { |
|
position: "inner", |
|
margin: 10, |
|
attrs: { |
|
"font-size": 15, |
|
fill: "#c7c7c7" |
|
}, |
|
attrsHover: { |
|
fill: "#eaeaea", |
|
"animDuration": 300 |
|
} |
|
}, |
|
target: "_self", |
|
cssClass: "area" |
|
}, |
|
defaultPlot: { |
|
type: "circle", |
|
size: 15, |
|
attrs: { |
|
fill: "#0088db", |
|
stroke: "#fff", |
|
"stroke-width": 0, |
|
"stroke-linejoin": "round" |
|
}, |
|
attrsHover: { |
|
"stroke-width": 3, |
|
animDuration: 300 |
|
}, |
|
text: { |
|
position: "right", |
|
margin: 10, |
|
attrs: { |
|
"font-size": 15, |
|
fill: "#c7c7c7" |
|
}, |
|
attrsHover: { |
|
fill: "#eaeaea", |
|
animDuration: 300 |
|
} |
|
}, |
|
target: "_self", |
|
cssClass: "plot" |
|
}, |
|
defaultLink: { |
|
factor: 0.5, |
|
attrs: { |
|
stroke: "#0088db", |
|
"stroke-width": 2 |
|
}, |
|
attrsHover: { |
|
animDuration: 300 |
|
}, |
|
text: { |
|
position: "inner", |
|
margin: 10, |
|
attrs: { |
|
"font-size": 15, |
|
fill: "#c7c7c7" |
|
}, |
|
attrsHover: { |
|
fill: "#eaeaea", |
|
animDuration: 300 |
|
} |
|
}, |
|
target: "_self", |
|
cssClass: "link" |
|
}, |
|
zoom: { |
|
enabled: false, |
|
minLevel: 0, |
|
maxLevel: 10, |
|
step: 0.25, |
|
mousewheel: true, |
|
touch: true, |
|
animDuration: 200, |
|
animEasing: "linear", |
|
buttons: { |
|
"reset": { |
|
cssClass: "zoomButton zoomReset", |
|
content: "•", // bullet sign |
|
title: "Reset zoom" |
|
}, |
|
"in": { |
|
cssClass: "zoomButton zoomIn", |
|
content: "+", |
|
title: "Zoom in" |
|
}, |
|
"out": { |
|
cssClass: "zoomButton zoomOut", |
|
content: "−", // minus sign |
|
title: "Zoom out" |
|
} |
|
} |
|
} |
|
}, |
|
legend: { |
|
redrawOnResize: true, |
|
area: [], |
|
plot: [] |
|
}, |
|
areas: {}, |
|
plots: {}, |
|
links: {} |
|
}, |
|
|
|
// Default legends option |
|
legendDefaultOptions: { |
|
area: { |
|
cssClass: "areaLegend", |
|
display: true, |
|
marginLeft: 10, |
|
marginLeftTitle: 5, |
|
marginBottomTitle: 10, |
|
marginLeftLabel: 10, |
|
marginBottom: 10, |
|
titleAttrs: { |
|
"font-size": 16, |
|
fill: "#343434", |
|
"text-anchor": "start" |
|
}, |
|
labelAttrs: { |
|
"font-size": 12, |
|
fill: "#343434", |
|
"text-anchor": "start" |
|
}, |
|
labelAttrsHover: { |
|
fill: "#787878", |
|
animDuration: 300 |
|
}, |
|
hideElemsOnClick: { |
|
enabled: true, |
|
opacity: 0.2, |
|
animDuration: 300 |
|
}, |
|
slices: [], |
|
mode: "vertical" |
|
}, |
|
plot: { |
|
cssClass: "plotLegend", |
|
display: true, |
|
marginLeft: 10, |
|
marginLeftTitle: 5, |
|
marginBottomTitle: 10, |
|
marginLeftLabel: 10, |
|
marginBottom: 10, |
|
titleAttrs: { |
|
"font-size": 16, |
|
fill: "#343434", |
|
"text-anchor": "start" |
|
}, |
|
labelAttrs: { |
|
"font-size": 12, |
|
fill: "#343434", |
|
"text-anchor": "start" |
|
}, |
|
labelAttrsHover: { |
|
fill: "#787878", |
|
animDuration: 300 |
|
}, |
|
hideElemsOnClick: { |
|
enabled: true, |
|
opacity: 0.2, |
|
animDuration: 300 |
|
}, |
|
slices: [], |
|
mode: "vertical" |
|
} |
|
} |
|
|
|
}; |
|
|
|
// Mapael version number |
|
// Accessible as $.mapael.version |
|
Mapael.version = version; |
|
|
|
// Extend jQuery with Mapael |
|
if ($[pluginName] === undefined) $[pluginName] = Mapael; |
|
|
|
// Add jQuery DOM function |
|
$.fn[pluginName] = function (options) { |
|
// Call Mapael on each element |
|
return this.each(function () { |
|
// Avoid leaking problem on multiple instanciation by removing an old mapael object on a container |
|
if ($.data(this, pluginName)) { |
|
$.data(this, pluginName).destroy(); |
|
} |
|
// Create Mapael and save it as jQuery data |
|
// This allow external access to Mapael using $(".mapcontainer").data("mapael") |
|
$.data(this, pluginName, new Mapael(this, options)); |
|
}); |
|
}; |
|
|
|
return Mapael; |
|
|
|
}));
|
|
|