"use strict";
var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };
    return function (d, b) {
        if (typeof b !== "function" && b !== null)
            throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
var __assign = (this && this.__assign) || function () {
    __assign = Object.assign || function(t) {
        for (var s, i = 1, n = arguments.length; i < n; i++) {
            s = arguments[i];
            for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
                t[p] = s[p];
        }
        return t;
    };
    return __assign.apply(this, arguments);
};
var __read = (this && this.__read) || function (o, n) {
    var m = typeof Symbol === "function" && o[Symbol.iterator];
    if (!m) return o;
    var i = m.call(o), r, ar = [], e;
    try {
        while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
    }
    catch (error) { e = { error: error }; }
    finally {
        try {
            if (r && !r.done && (m = i["return"])) m.call(i);
        }
        finally { if (e) throw e.error; }
    }
    return ar;
};
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
    if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
        if (ar || !(i in from)) {
            if (!ar) ar = Array.prototype.slice.call(from, 0, i);
            ar[i] = from[i];
        }
    }
    return to.concat(ar || Array.prototype.slice.call(from));
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
var camera_1 = __importDefault(require("./core/camera"));
var mouse_1 = __importDefault(require("./core/captors/mouse"));
var types_1 = require("./types");
var utils_1 = require("./utils");
var labels_1 = require("./core/labels");
var settings_1 = require("./settings");
var touch_1 = __importDefault(require("./core/captors/touch"));
var matrices_1 = require("./utils/matrices");
var array_1 = require("./utils/array");
var picking_1 = require("./utils/picking");
/**
 * Constants.
 */
var X_LABEL_MARGIN = 150;
var Y_LABEL_MARGIN = 50;
/**
 * Important functions.
 */
function applyNodeDefaults(settings, key, data) {
    if (!data.hasOwnProperty("x") || !data.hasOwnProperty("y"))
        throw new Error("Sigma: could not find a valid position (x, y) for node \"".concat(key, "\". All your nodes must have a number \"x\" and \"y\". Maybe your forgot to apply a layout or your \"nodeReducer\" is not returning the correct data?"));
    if (!data.color)
        data.color = settings.defaultNodeColor;
    if (!data.label && data.label !== "")
        data.label = null;
    if (data.label !== undefined && data.label !== null)
        data.label = "" + data.label;
    else
        data.label = null;
    if (!data.size)
        data.size = 2;
    if (!data.hasOwnProperty("hidden"))
        data.hidden = false;
    if (!data.hasOwnProperty("highlighted"))
        data.highlighted = false;
    if (!data.hasOwnProperty("forceLabel"))
        data.forceLabel = false;
    if (!data.type || data.type === "")
        data.type = settings.defaultNodeType;
    if (!data.zIndex)
        data.zIndex = 0;
    return data;
}
function applyEdgeDefaults(settings, _key, data) {
    if (!data.color)
        data.color = settings.defaultEdgeColor;
    if (!data.label)
        data.label = "";
    if (!data.size)
        data.size = 0.5;
    if (!data.hasOwnProperty("hidden"))
        data.hidden = false;
    if (!data.hasOwnProperty("forceLabel"))
        data.forceLabel = false;
    if (!data.type || data.type === "")
        data.type = settings.defaultEdgeType;
    if (!data.zIndex)
        data.zIndex = 0;
    return data;
}
/**
 * Main class.
 *
 * @constructor
 * @param {Graph}       graph     - Graph to render.
 * @param {HTMLElement} container - DOM container in which to render.
 * @param {object}      settings  - Optional settings.
 */
var Sigma = /** @class */ (function (_super) {
    __extends(Sigma, _super);
    function Sigma(graph, container, settings) {
        if (settings === void 0) { settings = {}; }
        var _this = _super.call(this) || this;
        _this.elements = {};
        _this.canvasContexts = {};
        _this.webGLContexts = {};
        _this.pickingLayers = new Set();
        _this.textures = {};
        _this.frameBuffers = {};
        _this.activeListeners = {};
        _this.labelGrid = new labels_1.LabelGrid();
        _this.nodeDataCache = {};
        _this.edgeDataCache = {};
        // Indices to keep track of the index of the item inside programs
        _this.nodeProgramIndex = {};
        _this.edgeProgramIndex = {};
        _this.nodesWithForcedLabels = new Set();
        _this.edgesWithForcedLabels = new Set();
        _this.nodeExtent = { x: [0, 1], y: [0, 1] };
        _this.nodeZExtent = [Infinity, -Infinity];
        _this.edgeZExtent = [Infinity, -Infinity];
        _this.matrix = (0, matrices_1.identity)();
        _this.invMatrix = (0, matrices_1.identity)();
        _this.correctionRatio = 1;
        _this.customBBox = null;
        _this.normalizationFunction = (0, utils_1.createNormalizationFunction)({
            x: [0, 1],
            y: [0, 1],
        });
        // Cache:
        _this.graphToViewportRatio = 1;
        _this.itemIDsIndex = {};
        _this.nodeIndices = {};
        _this.edgeIndices = {};
        // Starting dimensions and pixel ratio
        _this.width = 0;
        _this.height = 0;
        _this.pixelRatio = (0, utils_1.getPixelRatio)();
        _this.pickingDownSizingRatio = 2 * _this.pixelRatio;
        // Graph State
        _this.displayedNodeLabels = new Set();
        _this.displayedEdgeLabels = new Set();
        _this.highlightedNodes = new Set();
        _this.hoveredNode = null;
        _this.hoveredEdge = null;
        // Internal states
        _this.renderFrame = null;
        _this.renderHighlightedNodesFrame = null;
        _this.needToProcess = false;
        _this.checkEdgesEventsFrame = null;
        // Programs
        _this.nodePrograms = {};
        _this.nodeHoverPrograms = {};
        _this.edgePrograms = {};
        // Resolving settings
        _this.settings = (0, settings_1.resolveSettings)(settings);
        // Validating
        (0, settings_1.validateSettings)(_this.settings);
        (0, utils_1.validateGraph)(graph);
        if (!(container instanceof HTMLElement))
            throw new Error("Sigma: container should be an html element.");
        // Properties
        _this.graph = graph;
        _this.container = container;
        // Initializing contexts
        _this.createWebGLContext("edges", { picking: settings.enableEdgeEvents });
        _this.createCanvasContext("edgeLabels");
        _this.createWebGLContext("nodes", { picking: true });
        _this.createCanvasContext("labels");
        _this.createCanvasContext("hovers");
        _this.createWebGLContext("hoverNodes");
        _this.createCanvasContext("mouse");
        // Initial resize
        _this.resize();
        // Loading programs
        for (var type in _this.settings.nodeProgramClasses) {
            var NodeProgramClass = _this.settings.nodeProgramClasses[type];
            _this.nodePrograms[type] = new NodeProgramClass(_this.webGLContexts.nodes, _this.frameBuffers.nodes, _this);
            var NodeHoverProgram = NodeProgramClass;
            if (type in _this.settings.nodeHoverProgramClasses) {
                NodeHoverProgram = _this.settings.nodeHoverProgramClasses[type];
            }
            _this.nodeHoverPrograms[type] = new NodeHoverProgram(_this.webGLContexts.hoverNodes, null, _this);
        }
        for (var type in _this.settings.edgeProgramClasses) {
            var EdgeProgramClass = _this.settings.edgeProgramClasses[type];
            _this.edgePrograms[type] = new EdgeProgramClass(_this.webGLContexts.edges, _this.frameBuffers.edges, _this);
        }
        // Initializing the camera
        _this.camera = new camera_1.default();
        // Binding camera events
        _this.bindCameraHandlers();
        // Initializing captors
        _this.mouseCaptor = new mouse_1.default(_this.elements.mouse, _this);
        _this.touchCaptor = new touch_1.default(_this.elements.mouse, _this);
        // Binding event handlers
        _this.bindEventHandlers();
        // Binding graph handlers
        _this.bindGraphHandlers();
        // Trigger eventual settings-related things
        _this.handleSettingsUpdate();
        // Processing data for the first time & render
        _this.refresh();
        return _this;
    }
    /**---------------------------------------------------------------------------
     * Internal methods.
     **---------------------------------------------------------------------------
     */
    /**
     * Internal function used to create a canvas element.
     * @param  {string} id - Context's id.
     * @return {Sigma}
     */
    Sigma.prototype.createCanvas = function (id) {
        var canvas = (0, utils_1.createElement)("canvas", {
            position: "absolute",
        }, {
            class: "sigma-".concat(id),
        });
        this.elements[id] = canvas;
        this.container.appendChild(canvas);
        return canvas;
    };
    /**
     * Internal function used to create a canvas context and add the relevant
     * DOM elements.
     *
     * @param  {string} id - Context's id.
     * @return {Sigma}
     */
    Sigma.prototype.createCanvasContext = function (id) {
        var canvas = this.createCanvas(id);
        var contextOptions = {
            preserveDrawingBuffer: false,
            antialias: false,
        };
        this.canvasContexts[id] = canvas.getContext("2d", contextOptions);
        return this;
    };
    /**
     * Internal function used to create a WebGL context and add the relevant DOM
     * elements.
     *
     * @param  {string}  id      - Context's id.
     * @param  {object?} options - #getContext params to override (optional)
     * @return {Sigma}
     */
    Sigma.prototype.createWebGLContext = function (id, options) {
        var canvas = this.createCanvas(id);
        if (options === null || options === void 0 ? void 0 : options.hidden)
            canvas.remove();
        var contextOptions = __assign({ preserveDrawingBuffer: false, antialias: false }, (options || {}));
        var context;
        // First we try webgl2 for an easy performance boost
        context = canvas.getContext("webgl2", contextOptions);
        // Else we fall back to webgl
        if (!context)
            context = canvas.getContext("webgl", contextOptions);
        // Edge, I am looking right at you...
        if (!context)
            context = canvas.getContext("experimental-webgl", contextOptions);
        var gl = context;
        this.webGLContexts[id] = gl;
        // Blending:
        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
        // Prepare frame buffer for picking layers:
        if (options === null || options === void 0 ? void 0 : options.picking) {
            this.pickingLayers.add(id);
            var newFrameBuffer = gl.createFramebuffer();
            if (!newFrameBuffer)
                throw new Error("Sigma: cannot create a new frame buffer for layer ".concat(id));
            this.frameBuffers[id] = newFrameBuffer;
        }
        return this;
    };
    /**
     * Method (re)binding WebGL texture (for picking).
     *
     * @return {Sigma}
     */
    Sigma.prototype.resetWebGLTexture = function (id) {
        var gl = this.webGLContexts[id];
        var frameBuffer = this.frameBuffers[id];
        var currentTexture = this.textures[id];
        if (currentTexture)
            gl.deleteTexture(currentTexture);
        var pickingTexture = gl.createTexture();
        gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer);
        gl.bindTexture(gl.TEXTURE_2D, pickingTexture);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.width / this.pickingDownSizingRatio, this.height / this.pickingDownSizingRatio, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
        gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, pickingTexture, 0);
        this.textures[id] = pickingTexture;
        return this;
    };
    /**
     * Method binding camera handlers.
     *
     * @return {Sigma}
     */
    Sigma.prototype.bindCameraHandlers = function () {
        var _this = this;
        this.activeListeners.camera = function () {
            _this.scheduleRender();
        };
        this.camera.on("updated", this.activeListeners.camera);
        return this;
    };
    /**
     * Method unbinding camera handlers.
     *
     * @return {Sigma}
     */
    Sigma.prototype.unbindCameraHandlers = function () {
        this.camera.removeListener("updated", this.activeListeners.camera);
        return this;
    };
    /**
     * Method that returns the closest node to a given position.
     */
    Sigma.prototype.getNodeAtPosition = function (position) {
        var x = position.x, y = position.y;
        var color = (0, picking_1.getPixelColor)(this.webGLContexts.nodes, this.frameBuffers.nodes, x, y, this.pickingDownSizingRatio);
        var index = utils_1.colorToIndex.apply(void 0, __spreadArray([], __read(color), false));
        var itemAt = this.itemIDsIndex[index];
        return itemAt && itemAt.type === "node" ? itemAt.id : null;
    };
    /**
     * Method binding event handlers.
     *
     * @return {Sigma}
     */
    Sigma.prototype.bindEventHandlers = function () {
        var _this = this;
        // Handling window resize
        this.activeListeners.handleResize = function () {
            // need to call a refresh to rebuild the labelgrid
            _this.scheduleRefresh();
        };
        window.addEventListener("resize", this.activeListeners.handleResize);
        // Handling mouse move
        this.activeListeners.handleMove = function (e) {
            var baseEvent = {
                event: e,
                preventSigmaDefault: function () {
                    e.preventSigmaDefault();
                },
            };
            var nodeToHover = _this.getNodeAtPosition(e);
            if (nodeToHover && _this.hoveredNode !== nodeToHover && !_this.nodeDataCache[nodeToHover].hidden) {
                // Handling passing from one node to the other directly
                if (_this.hoveredNode)
                    _this.emit("leaveNode", __assign(__assign({}, baseEvent), { node: _this.hoveredNode }));
                _this.hoveredNode = nodeToHover;
                _this.emit("enterNode", __assign(__assign({}, baseEvent), { node: nodeToHover }));
                _this.scheduleHighlightedNodesRender();
                return;
            }
            // Checking if the hovered node is still hovered
            if (_this.hoveredNode) {
                if (_this.getNodeAtPosition(e) !== _this.hoveredNode) {
                    var node = _this.hoveredNode;
                    _this.hoveredNode = null;
                    _this.emit("leaveNode", __assign(__assign({}, baseEvent), { node: node }));
                    _this.scheduleHighlightedNodesRender();
                    return;
                }
            }
            if (_this.settings.enableEdgeEvents) {
                _this.checkEdgeHoverEvents(baseEvent);
            }
        };
        // Handling click
        var createMouseListener = function (eventType) {
            return function (e) {
                var baseEvent = {
                    event: e,
                    preventSigmaDefault: function () {
                        e.preventSigmaDefault();
                    },
                };
                var isFakeSigmaMouseEvent = e.original.isFakeSigmaMouseEvent;
                var nodeAtPosition = isFakeSigmaMouseEvent ? _this.getNodeAtPosition(e) : _this.hoveredNode;
                if (nodeAtPosition)
                    return _this.emit("".concat(eventType, "Node"), __assign(__assign({}, baseEvent), { node: nodeAtPosition }));
                if (_this.settings.enableEdgeEvents) {
                    var edge = _this.getEdgeAtPoint(e.x, e.y);
                    if (edge)
                        return _this.emit("".concat(eventType, "Edge"), __assign(__assign({}, baseEvent), { edge: edge }));
                }
                return _this.emit("".concat(eventType, "Stage"), baseEvent);
            };
        };
        this.activeListeners.handleClick = createMouseListener("click");
        this.activeListeners.handleRightClick = createMouseListener("rightClick");
        this.activeListeners.handleDoubleClick = createMouseListener("doubleClick");
        this.activeListeners.handleWheel = createMouseListener("wheel");
        this.activeListeners.handleDown = createMouseListener("down");
        this.mouseCaptor.on("mousemove", this.activeListeners.handleMove);
        this.mouseCaptor.on("click", this.activeListeners.handleClick);
        this.mouseCaptor.on("rightClick", this.activeListeners.handleRightClick);
        this.mouseCaptor.on("doubleClick", this.activeListeners.handleDoubleClick);
        this.mouseCaptor.on("wheel", this.activeListeners.handleWheel);
        this.mouseCaptor.on("mousedown", this.activeListeners.handleDown);
        // TODO
        // Deal with Touch captor events
        return this;
    };
    /**
     * Method binding graph handlers
     *
     * @return {Sigma}
     */
    Sigma.prototype.bindGraphHandlers = function () {
        var _this = this;
        var graph = this.graph;
        var LAYOUT_IMPACTING_FIELDS = new Set(["x", "y", "zIndex", "type"]);
        this.activeListeners.eachNodeAttributesUpdatedGraphUpdate = function (e) {
            var _a;
            var updatedFields = (_a = e.hints) === null || _a === void 0 ? void 0 : _a.attributes;
            // we process all nodes
            _this.graph.forEachNode(function (node) { return _this.updateNode(node); });
            // if coord, type or zIndex have changed, we need to schedule a render
            // (zIndex for the programIndex)
            var layoutChanged = !updatedFields || updatedFields.some(function (f) { return LAYOUT_IMPACTING_FIELDS.has(f); });
            _this.refresh({ partialGraph: { nodes: graph.nodes() }, skipIndexation: !layoutChanged, schedule: true });
        };
        this.activeListeners.eachEdgeAttributesUpdatedGraphUpdate = function (e) {
            var _a;
            var updatedFields = (_a = e.hints) === null || _a === void 0 ? void 0 : _a.attributes;
            // we process all edges
            _this.graph.forEachEdge(function (edge) { return _this.updateEdge(edge); });
            var layoutChanged = updatedFields && ["zIndex", "type"].some(function (f) { return updatedFields === null || updatedFields === void 0 ? void 0 : updatedFields.includes(f); });
            _this.refresh({ partialGraph: { edges: graph.edges() }, skipIndexation: !layoutChanged, schedule: true });
        };
        // On add node, we add the node in indices and then call for a render
        this.activeListeners.addNodeGraphUpdate = function (payload) {
            var node = payload.key;
            // we process the node
            _this.addNode(node);
            // schedule a render for the node
            _this.refresh({ partialGraph: { nodes: [node] }, skipIndexation: false, schedule: true });
        };
        // On update node, we update indices and then call for a render
        this.activeListeners.updateNodeGraphUpdate = function (payload) {
            var node = payload.key;
            // schedule a render for the node
            _this.refresh({ partialGraph: { nodes: [node] }, skipIndexation: false, schedule: true });
        };
        // On drop node, we remove the node from indices and then call for a refresh
        this.activeListeners.dropNodeGraphUpdate = function (payload) {
            var node = payload.key;
            // we process the node
            _this.removeNode(node);
            // schedule a render for everything
            _this.refresh({ schedule: true });
        };
        // On add edge, we remove the edge from indices and then call for a refresh
        this.activeListeners.addEdgeGraphUpdate = function (payload) {
            var edge = payload.key;
            // we process the edge
            _this.addEdge(edge);
            // schedule a render for the edge
            _this.refresh({ partialGraph: { edges: [edge] }, schedule: true });
        };
        // On update edge, we update indices and then call for a refresh
        this.activeListeners.updateEdgeGraphUpdate = function (payload) {
            var edge = payload.key;
            // schedule a repaint for the edge
            _this.refresh({ partialGraph: { edges: [edge] }, skipIndexation: false, schedule: true });
        };
        // On drop edge, we remove the edge from indices and then call for a refresh
        this.activeListeners.dropEdgeGraphUpdate = function (payload) {
            var edge = payload.key;
            // we process the edge
            _this.removeEdge(edge);
            // schedule a render for all edges
            _this.refresh({ schedule: true });
        };
        // On clear edges, we clear the edge indices and then call for a refresh
        this.activeListeners.clearEdgesGraphUpdate = function () {
            // we clear the edge data structures
            _this.clearEdgeState();
            _this.clearEdgeIndices();
            // schedule a render for all edges
            _this.refresh({ schedule: true });
        };
        // On graph clear, we clear indices and then call for a refresh
        this.activeListeners.clearGraphUpdate = function () {
            // clear graph state
            _this.clearEdgeState();
            _this.clearNodeState();
            // clear graph indices
            _this.clearEdgeIndices();
            _this.clearNodeIndices();
            // schedule a render for all
            _this.refresh({ schedule: true });
        };
        graph.on("nodeAdded", this.activeListeners.addNodeGraphUpdate); // OK
        graph.on("nodeDropped", this.activeListeners.dropNodeGraphUpdate); // OK
        graph.on("nodeAttributesUpdated", this.activeListeners.updateNodeGraphUpdate); // OK
        graph.on("eachNodeAttributesUpdated", this.activeListeners.eachNodeAttributesUpdatedGraphUpdate);
        graph.on("edgeAdded", this.activeListeners.addEdgeGraphUpdate); //OK
        graph.on("edgeDropped", this.activeListeners.dropEdgeGraphUpdate); //OK
        graph.on("edgeAttributesUpdated", this.activeListeners.updateEdgeGraphUpdate); //OK
        graph.on("eachEdgeAttributesUpdated", this.activeListeners.eachEdgeAttributesUpdatedGraphUpdate);
        graph.on("edgesCleared", this.activeListeners.clearEdgesGraphUpdate); //OK
        graph.on("cleared", this.activeListeners.clearGraphUpdate); //OK
        return this;
    };
    /**
     * Method used to unbind handlers from the graph.
     *
     * @return {undefined}
     */
    Sigma.prototype.unbindGraphHandlers = function () {
        var graph = this.graph;
        graph.removeListener("nodeAdded", this.activeListeners.graphUpdate);
        graph.removeListener("nodeDropped", this.activeListeners.dropNodeGraphUpdate);
        graph.removeListener("nodeAttributesUpdated", this.activeListeners.graphUpdate);
        graph.removeListener("eachNodeAttributesUpdated", this.activeListeners.graphUpdate);
        graph.removeListener("edgeAdded", this.activeListeners.graphUpdate);
        graph.removeListener("edgeDropped", this.activeListeners.dropEdgeGraphUpdate);
        graph.removeListener("edgeAttributesUpdated", this.activeListeners.graphUpdate);
        graph.removeListener("eachEdgeAttributesUpdated", this.activeListeners.graphUpdate);
        graph.removeListener("edgesCleared", this.activeListeners.clearEdgesGraphUpdate);
        graph.removeListener("cleared", this.activeListeners.clearGraphUpdate);
    };
    /**
     * Method dealing with "leaveEdge" and "enterEdge" events.
     *
     * @return {Sigma}
     */
    Sigma.prototype.checkEdgeHoverEvents = function (payload) {
        var edgeToHover = this.hoveredNode ? null : this.getEdgeAtPoint(payload.event.x, payload.event.y);
        if (edgeToHover !== this.hoveredEdge) {
            if (this.hoveredEdge)
                this.emit("leaveEdge", __assign(__assign({}, payload), { edge: this.hoveredEdge }));
            if (edgeToHover)
                this.emit("enterEdge", __assign(__assign({}, payload), { edge: edgeToHover }));
            this.hoveredEdge = edgeToHover;
        }
        return this;
    };
    /**
     * Method looking for an edge colliding with a given point at (x, y). Returns
     * the key of the edge if any, or null else.
     */
    Sigma.prototype.getEdgeAtPoint = function (x, y) {
        var color = (0, picking_1.getPixelColor)(this.webGLContexts.edges, this.frameBuffers.edges, x, y, this.pickingDownSizingRatio);
        var index = utils_1.colorToIndex.apply(void 0, __spreadArray([], __read(color), false));
        var itemAt = this.itemIDsIndex[index];
        return itemAt && itemAt.type === "edge" ? itemAt.id : null;
    };
    /**
     * Method used to process the whole graph's data.
     *  - extent
     *  - normalizationFunction
     *  - compute node's coordinate
     *  - labelgrid
     *  - program data allocation
     * @return {Sigma}
     */
    Sigma.prototype.process = function () {
        var _this = this;
        var graph = this.graph;
        var settings = this.settings;
        var dimensions = this.getDimensions();
        //
        // NODES
        //
        this.nodeExtent = (0, utils_1.graphExtent)(this.graph);
        this.normalizationFunction = (0, utils_1.createNormalizationFunction)(this.customBBox || this.nodeExtent);
        // NOTE: it is important to compute this matrix after computing the node's extent
        // because #.getGraphDimensions relies on it
        var nullCamera = new camera_1.default();
        var nullCameraMatrix = (0, utils_1.matrixFromCamera)(nullCamera.getState(), dimensions, this.getGraphDimensions(), this.getSetting("stagePadding") || 0);
        // Resetting the label grid
        // TODO: it's probably better to do this explicitly or on resizes for layout and anims
        this.labelGrid.resizeAndClear(dimensions, settings.labelGridCellSize);
        var nodesPerPrograms = {};
        var nodeIndices = {};
        var edgeIndices = {};
        var itemIDsIndex = {};
        var incrID = 1;
        var nodes = graph.nodes();
        // Do some indexation on the whole graph
        for (var i = 0, l = nodes.length; i < l; i++) {
            var node = nodes[i];
            var data = this.nodeDataCache[node];
            // Get initial coordinates
            var attrs = graph.getNodeAttributes(node);
            data.x = attrs.x;
            data.y = attrs.y;
            this.normalizationFunction.applyTo(data);
            // labelgrid
            if (typeof data.label === "string" && !data.hidden)
                this.labelGrid.add(node, data.size, this.framedGraphToViewport(data, { matrix: nullCameraMatrix }));
            // update count per program
            nodesPerPrograms[data.type] = (nodesPerPrograms[data.type] || 0) + 1;
        }
        this.labelGrid.organize();
        // Allocate memory to programs
        for (var type in this.nodePrograms) {
            if (!this.nodePrograms.hasOwnProperty(type)) {
                throw new Error("Sigma: could not find a suitable program for node type \"".concat(type, "\"!"));
            }
            this.nodePrograms[type].reallocate(nodesPerPrograms[type] || 0);
            // We reset that count here, so that we can reuse it while calling the Program#process methods:
            nodesPerPrograms[type] = 0;
        }
        // Order nodes by zIndex before to add them to program
        if (this.settings.zIndex && this.nodeZExtent[0] !== this.nodeZExtent[1])
            nodes = (0, utils_1.zIndexOrdering)(this.nodeZExtent, function (node) { return _this.nodeDataCache[node].zIndex; }, nodes);
        // Add data to programs
        for (var i = 0, l = nodes.length; i < l; i++) {
            var node = nodes[i];
            itemIDsIndex[incrID] = { type: "node", id: node };
            nodeIndices[node] = incrID;
            incrID++;
            var data = this.nodeDataCache[node];
            this.addNodeToProgram(node, nodeIndices[node], nodesPerPrograms[data.type]++);
        }
        //
        // EDGES
        //
        var edgesPerPrograms = {};
        var edges = graph.edges();
        // Allocate memory to programs
        for (var i = 0, l = edges.length; i < l; i++) {
            var edge = edges[i];
            var data = this.edgeDataCache[edge];
            edgesPerPrograms[data.type] = (edgesPerPrograms[data.type] || 0) + 1;
        }
        // Order edges by zIndex before to add them to program
        if (this.settings.zIndex && this.edgeZExtent[0] !== this.edgeZExtent[1])
            edges = (0, utils_1.zIndexOrdering)(this.edgeZExtent, function (edge) { return _this.edgeDataCache[edge].zIndex; }, edges);
        for (var type in this.edgePrograms) {
            if (!this.edgePrograms.hasOwnProperty(type)) {
                throw new Error("Sigma: could not find a suitable program for edge type \"".concat(type, "\"!"));
            }
            this.edgePrograms[type].reallocate(edgesPerPrograms[type] || 0);
            // We reset that count here, so that we can reuse it while calling the Program#process methods:
            edgesPerPrograms[type] = 0;
        }
        // Add data to programs
        for (var i = 0, l = edges.length; i < l; i++) {
            var edge = edges[i];
            itemIDsIndex[incrID] = { type: "edge", id: edge };
            edgeIndices[edge] = incrID;
            incrID++;
            var data = this.edgeDataCache[edge];
            this.addEdgeToProgram(edge, edgeIndices[edge], edgesPerPrograms[data.type]++);
        }
        this.itemIDsIndex = itemIDsIndex;
        this.nodeIndices = nodeIndices;
        this.edgeIndices = edgeIndices;
        return this;
    };
    /**
     * Method that backports potential settings updates where it's needed.
     * @private
     */
    Sigma.prototype.handleSettingsUpdate = function () {
        this.camera.minRatio = this.settings.minCameraRatio;
        this.camera.maxRatio = this.settings.maxCameraRatio;
        this.camera.setState(this.camera.validateState(this.camera.getState()));
        return this;
    };
    /**
     * Method used to render labels.
     *
     * @return {Sigma}
     */
    Sigma.prototype.renderLabels = function () {
        if (!this.settings.renderLabels)
            return this;
        var cameraState = this.camera.getState();
        // Selecting labels to draw
        var labelsToDisplay = this.labelGrid.getLabelsToDisplay(cameraState.ratio, this.settings.labelDensity);
        (0, array_1.extend)(labelsToDisplay, this.nodesWithForcedLabels);
        this.displayedNodeLabels = new Set();
        // Drawing labels
        var context = this.canvasContexts.labels;
        for (var i = 0, l = labelsToDisplay.length; i < l; i++) {
            var node = labelsToDisplay[i];
            var data = this.nodeDataCache[node];
            // If the node was already drawn (like if it is eligible AND has
            // `forceLabel`), we don't want to draw it again
            // NOTE: we can do better probably
            if (this.displayedNodeLabels.has(node))
                continue;
            // If the node is hidden, we don't need to display its label obviously
            if (data.hidden)
                continue;
            var _a = this.framedGraphToViewport(data), x = _a.x, y = _a.y;
            // NOTE: we can cache the labels we need to render until the camera's ratio changes
            var size = this.scaleSize(data.size);
            // Is node big enough?
            if (!data.forceLabel && size < this.settings.labelRenderedSizeThreshold)
                continue;
            // Is node actually on screen (with some margin)
            // NOTE: we used to rely on the quadtree for this, but the coordinates
            // conversion make it unreliable and at that point we already converted
            // to viewport coordinates and since the label grid already culls the
            // number of potential labels to display this looks like a good
            // performance compromise.
            // NOTE: labelGrid.getLabelsToDisplay could probably optimize by not
            // considering cells obviously outside of the range of the current
            // view rectangle.
            if (x < -X_LABEL_MARGIN ||
                x > this.width + X_LABEL_MARGIN ||
                y < -Y_LABEL_MARGIN ||
                y > this.height + Y_LABEL_MARGIN)
                continue;
            // Because displayed edge labels depend directly on actually rendered node
            // labels, we need to only add to this.displayedNodeLabels nodes whose label
            // is rendered.
            // This makes this.displayedNodeLabels depend on viewport, which might become
            // an issue once we start memoizing getLabelsToDisplay.
            this.displayedNodeLabels.add(node);
            var _b = this.settings, nodeProgramClasses = _b.nodeProgramClasses, defaultDrawNodeLabel = _b.defaultDrawNodeLabel;
            var drawLabel = nodeProgramClasses[data.type].drawLabel || defaultDrawNodeLabel;
            drawLabel(context, __assign(__assign({ key: node }, data), { size: size, x: x, y: y }), this.settings);
        }
        return this;
    };
    /**
     * Method used to render edge labels, based on which node labels were
     * rendered.
     *
     * @return {Sigma}
     */
    Sigma.prototype.renderEdgeLabels = function () {
        if (!this.settings.renderEdgeLabels)
            return this;
        var context = this.canvasContexts.edgeLabels;
        // Clearing
        context.clearRect(0, 0, this.width, this.height);
        var edgeLabelsToDisplay = (0, labels_1.edgeLabelsToDisplayFromNodes)({
            graph: this.graph,
            hoveredNode: this.hoveredNode,
            displayedNodeLabels: this.displayedNodeLabels,
            highlightedNodes: this.highlightedNodes,
        });
        (0, array_1.extend)(edgeLabelsToDisplay, this.edgesWithForcedLabels);
        var displayedLabels = new Set();
        for (var i = 0, l = edgeLabelsToDisplay.length; i < l; i++) {
            var edge = edgeLabelsToDisplay[i], extremities = this.graph.extremities(edge), sourceData = this.nodeDataCache[extremities[0]], targetData = this.nodeDataCache[extremities[1]], edgeData = this.edgeDataCache[edge];
            // If the edge was already drawn (like if it is eligible AND has
            // `forceLabel`), we don't want to draw it again
            if (displayedLabels.has(edge))
                continue;
            // If the edge is hidden we don't need to display its label
            // NOTE: the test on sourceData & targetData is probably paranoid at this point?
            if (edgeData.hidden || sourceData.hidden || targetData.hidden) {
                continue;
            }
            var _a = this.settings, edgeProgramClasses = _a.edgeProgramClasses, defaultDrawEdgeLabel = _a.defaultDrawEdgeLabel;
            var drawLabel = edgeProgramClasses[edgeData.type].drawLabel || defaultDrawEdgeLabel;
            drawLabel(context, __assign(__assign({ key: edge }, edgeData), { size: this.scaleSize(edgeData.size) }), __assign(__assign(__assign({ key: extremities[0] }, sourceData), this.framedGraphToViewport(sourceData)), { size: this.scaleSize(sourceData.size) }), __assign(__assign(__assign({ key: extremities[1] }, targetData), this.framedGraphToViewport(targetData)), { size: this.scaleSize(targetData.size) }), this.settings);
            displayedLabels.add(edge);
        }
        this.displayedEdgeLabels = displayedLabels;
        return this;
    };
    /**
     * Method used to render the highlighted nodes.
     *
     * @return {Sigma}
     */
    Sigma.prototype.renderHighlightedNodes = function () {
        var _this = this;
        var context = this.canvasContexts.hovers;
        // Clearing
        context.clearRect(0, 0, this.width, this.height);
        // Rendering
        var render = function (node) {
            var data = _this.nodeDataCache[node];
            var _a = _this.framedGraphToViewport(data), x = _a.x, y = _a.y;
            var size = _this.scaleSize(data.size);
            var _b = _this.settings, nodeProgramClasses = _b.nodeProgramClasses, defaultDrawNodeHover = _b.defaultDrawNodeHover;
            var drawHover = nodeProgramClasses[data.type].drawHover || defaultDrawNodeHover;
            drawHover(context, __assign(__assign({ key: node }, data), { size: size, x: x, y: y }), _this.settings);
        };
        var nodesToRender = [];
        if (this.hoveredNode && !this.nodeDataCache[this.hoveredNode].hidden) {
            nodesToRender.push(this.hoveredNode);
        }
        this.highlightedNodes.forEach(function (node) {
            // The hovered node has already been highlighted
            if (node !== _this.hoveredNode)
                nodesToRender.push(node);
        });
        // Draw labels:
        nodesToRender.forEach(function (node) { return render(node); });
        // Draw WebGL nodes on top of the labels:
        var nodesPerPrograms = {};
        // 1. Count nodes per type:
        nodesToRender.forEach(function (node) {
            var type = _this.nodeDataCache[node].type;
            nodesPerPrograms[type] = (nodesPerPrograms[type] || 0) + 1;
        });
        // 2. Allocate for each type for the proper number of nodes
        for (var type in this.nodeHoverPrograms) {
            this.nodeHoverPrograms[type].reallocate(nodesPerPrograms[type] || 0);
            // Also reset count, to use when rendering:
            nodesPerPrograms[type] = 0;
        }
        // 3. Process all nodes to render:
        nodesToRender.forEach(function (node) {
            var data = _this.nodeDataCache[node];
            _this.nodeHoverPrograms[data.type].process(0, nodesPerPrograms[data.type]++, data);
        });
        // 4. Clear hovered nodes layer:
        this.webGLContexts.hoverNodes.clear(this.webGLContexts.hoverNodes.COLOR_BUFFER_BIT);
        // 5. Render:
        for (var type in this.nodeHoverPrograms) {
            var program = this.nodeHoverPrograms[type];
            program.render({
                matrix: this.matrix,
                width: this.width,
                height: this.height,
                pixelRatio: this.pixelRatio,
                zoomRatio: this.camera.ratio,
                sizeRatio: 1 / this.scaleSize(),
                downSizedSizeRatio: 1 / this.scaleSize() / this.pickingDownSizingRatio,
                correctionRatio: this.correctionRatio,
                downSizingRatio: this.pickingDownSizingRatio,
            });
        }
    };
    /**
     * Method used to schedule a hover render.
     *
     */
    Sigma.prototype.scheduleHighlightedNodesRender = function () {
        var _this = this;
        if (this.renderHighlightedNodesFrame || this.renderFrame)
            return;
        this.renderHighlightedNodesFrame = (0, utils_1.requestFrame)(function () {
            // Resetting state
            _this.renderHighlightedNodesFrame = null;
            // Rendering
            _this.renderHighlightedNodes();
            _this.renderEdgeLabels();
        });
    };
    /**
     * Method used to render.
     *
     * @return {Sigma}
     */
    Sigma.prototype.render = function () {
        var _this = this;
        this.emit("beforeRender");
        var exitRender = function () {
            _this.emit("afterRender");
            return _this;
        };
        // If a render was scheduled, we cancel it
        if (this.renderFrame) {
            (0, utils_1.cancelFrame)(this.renderFrame);
            this.renderFrame = null;
        }
        // First we need to resize
        this.resize();
        // Do we need to reprocess data?
        if (this.needToProcess)
            this.process();
        this.needToProcess = false;
        // Clearing the canvases
        this.clear();
        // Prepare the textures
        this.pickingLayers.forEach(function (layer) { return _this.resetWebGLTexture(layer); });
        // If we have no nodes we can stop right there
        if (!this.graph.order)
            return exitRender();
        // TODO: improve this heuristic or move to the captor itself?
        // TODO: deal with the touch captor here as well
        var mouseCaptor = this.mouseCaptor;
        var moving = this.camera.isAnimated() ||
            mouseCaptor.isMoving ||
            mouseCaptor.draggedEvents ||
            mouseCaptor.currentWheelDirection;
        // Then we need to extract a matrix from the camera
        var cameraState = this.camera.getState();
        var viewportDimensions = this.getDimensions();
        var graphDimensions = this.getGraphDimensions();
        var padding = this.getSetting("stagePadding") || 0;
        this.matrix = (0, utils_1.matrixFromCamera)(cameraState, viewportDimensions, graphDimensions, padding);
        this.invMatrix = (0, utils_1.matrixFromCamera)(cameraState, viewportDimensions, graphDimensions, padding, true);
        this.correctionRatio = (0, utils_1.getMatrixImpact)(this.matrix, cameraState, viewportDimensions);
        this.graphToViewportRatio = this.getGraphToViewportRatio();
        // [jacomyal]
        // This comment is related to the one above the `getMatrixImpact` definition:
        // - `this.correctionRatio` is somehow not completely explained
        // - `this.graphToViewportRatio` is the ratio of a distance in the viewport divided by the same distance in the
        //   graph
        // - `this.normalizationFunction.ratio` is basically `Math.max(graphDX, graphDY)`
        // And now, I observe that if I multiply these three ratios, I have something constant, which value remains 2, even
        // when I change the graph, the viewport or the camera. It might be useful later so I prefer to let this comment:
        // console.log(this.graphToViewportRatio * this.correctionRatio * this.normalizationFunction.ratio * 2);
        var params = {
            matrix: this.matrix,
            width: this.width,
            height: this.height,
            pixelRatio: this.pixelRatio,
            zoomRatio: this.camera.ratio,
            sizeRatio: 1 / this.scaleSize(),
            downSizedSizeRatio: (1 / this.scaleSize()) * this.pickingDownSizingRatio,
            correctionRatio: this.correctionRatio,
            downSizingRatio: this.pickingDownSizingRatio,
        };
        // Drawing nodes
        for (var type in this.nodePrograms) {
            var program = this.nodePrograms[type];
            program.render(params);
        }
        // Drawing edges
        if (!this.settings.hideEdgesOnMove || !moving) {
            for (var type in this.edgePrograms) {
                var program = this.edgePrograms[type];
                program.render(params);
            }
        }
        // Do not display labels on move per setting
        if (this.settings.hideLabelsOnMove && moving)
            return exitRender();
        this.renderLabels();
        this.renderEdgeLabels();
        this.renderHighlightedNodes();
        return exitRender();
    };
    /**
     * Add a node in the internal data structures.
     * @private
     * @param key The node's graphology ID
     */
    Sigma.prototype.addNode = function (key) {
        // Node display data resolution:
        //  1. First we get the node's attributes
        //  2. We optionally reduce them using the function provided by the user
        //     Note that this function must return a total object and won't be merged
        //  3. We apply our defaults, while running some vital checks
        //  4. We apply the normalization function
        // We shallow copy node data to avoid dangerous behaviors from reducers
        var attr = Object.assign({}, this.graph.getNodeAttributes(key));
        if (this.settings.nodeReducer)
            attr = this.settings.nodeReducer(key, attr);
        var data = applyNodeDefaults(this.settings, key, attr);
        this.nodeDataCache[key] = data;
        // Label:
        // We delete and add if needed because this function is also used from
        // update
        this.nodesWithForcedLabels.delete(key);
        if (data.forceLabel && !data.hidden)
            this.nodesWithForcedLabels.add(key);
        // Highlighted:
        // We remove and re add if needed because this function is also used from
        // update
        this.highlightedNodes.delete(key);
        if (data.highlighted && !data.hidden)
            this.highlightedNodes.add(key);
        // zIndex
        if (this.settings.zIndex) {
            if (data.zIndex < this.nodeZExtent[0])
                this.nodeZExtent[0] = data.zIndex;
            if (data.zIndex > this.nodeZExtent[1])
                this.nodeZExtent[1] = data.zIndex;
        }
    };
    /**
     * Update a node the internal data structures.
     * @private
     * @param key The node's graphology ID
     */
    Sigma.prototype.updateNode = function (key) {
        this.addNode(key);
        // Re-apply normalization on the node
        var data = this.nodeDataCache[key];
        this.normalizationFunction.applyTo(data);
    };
    /**
     * Remove a node from the internal data structures.
     * @private
     * @param key The node's graphology ID
     */
    Sigma.prototype.removeNode = function (key) {
        // Remove from node cache
        delete this.nodeDataCache[key];
        // Remove from node program index
        delete this.nodeProgramIndex[key];
        // Remove from higlighted nodes
        this.highlightedNodes.delete(key);
        // Remove from hovered
        if (this.hoveredNode === key)
            this.hoveredNode = null;
        // Remove from forced label
        this.nodesWithForcedLabels.delete(key);
    };
    /**
     * Add an edge into the internal data structures.
     * @private
     * @param key The edge's graphology ID
     */
    Sigma.prototype.addEdge = function (key) {
        // Edge display data resolution:
        //  1. First we get the edge's attributes
        //  2. We optionally reduce them using the function provided by the user
        //  3. Note that this function must return a total object and won't be merged
        //  4. We apply our defaults, while running some vital checks
        // We shallow copy edge data to avoid dangerous behaviors from reducers
        var attr = Object.assign({}, this.graph.getEdgeAttributes(key));
        if (this.settings.edgeReducer)
            attr = this.settings.edgeReducer(key, attr);
        var data = applyEdgeDefaults(this.settings, key, attr);
        this.edgeDataCache[key] = data;
        // Forced label
        // we filter and re push if needed because this function is also used from
        // update
        this.edgesWithForcedLabels.delete(key);
        if (data.forceLabel && !data.hidden)
            this.edgesWithForcedLabels.add(key);
        // Check zIndex
        if (this.settings.zIndex) {
            if (data.zIndex < this.edgeZExtent[0])
                this.edgeZExtent[0] = data.zIndex;
            if (data.zIndex > this.edgeZExtent[1])
                this.edgeZExtent[1] = data.zIndex;
        }
    };
    /**
     * Update an edge in the internal data structures.
     * @private
     * @param key The edge's graphology ID
     */
    Sigma.prototype.updateEdge = function (key) {
        this.addEdge(key);
    };
    /**
     * Remove an edge from the internal data structures.
     * @private
     * @param key The edge's graphology ID
     */
    Sigma.prototype.removeEdge = function (key) {
        // Remove from edge cache
        delete this.edgeDataCache[key];
        // Remove from programId index
        delete this.edgeProgramIndex[key];
        // Remove from hovered
        if (this.hoveredEdge === key)
            this.hoveredEdge = null;
        // Remove from forced label
        this.edgesWithForcedLabels.delete(key);
    };
    /**
     * Clear all indices related to nodes.
     * @private
     */
    Sigma.prototype.clearNodeIndices = function () {
        // LabelGrid & nodeExtent are only manage/populated in the process function
        this.labelGrid = new labels_1.LabelGrid();
        this.nodeExtent = { x: [0, 1], y: [0, 1] };
        this.nodeDataCache = {};
        this.edgeProgramIndex = {};
        this.nodesWithForcedLabels = new Set();
        this.nodeZExtent = [Infinity, -Infinity];
    };
    /**
     * Clear all indices related to edges.
     * @private
     */
    Sigma.prototype.clearEdgeIndices = function () {
        this.edgeDataCache = {};
        this.edgeProgramIndex = {};
        this.edgesWithForcedLabels = new Set();
        this.edgeZExtent = [Infinity, -Infinity];
    };
    /**
     * Clear all indices.
     * @private
     */
    Sigma.prototype.clearIndices = function () {
        this.clearEdgeIndices();
        this.clearNodeIndices();
    };
    /**
     * Clear all graph state related to nodes.
     * @private
     */
    Sigma.prototype.clearNodeState = function () {
        this.displayedNodeLabels = new Set();
        this.highlightedNodes = new Set();
        this.hoveredNode = null;
    };
    /**
     * Clear all graph state related to edges.
     * @private
     */
    Sigma.prototype.clearEdgeState = function () {
        this.displayedEdgeLabels = new Set();
        this.highlightedNodes = new Set();
        this.hoveredEdge = null;
    };
    /**
     * Clear all graph state.
     * @private
     */
    Sigma.prototype.clearState = function () {
        this.clearEdgeState();
        this.clearNodeState();
    };
    /**
     * Add the node data to its program.
     * @private
     * @param node The node's graphology ID
     * @param fingerprint A fingerprint used to identity the node with picking
     * @param position The index where to place the node in the program
     */
    Sigma.prototype.addNodeToProgram = function (node, fingerprint, position) {
        var data = this.nodeDataCache[node];
        var nodeProgram = this.nodePrograms[data.type];
        if (!nodeProgram)
            throw new Error("Sigma: could not find a suitable program for node type \"".concat(data.type, "\"!"));
        nodeProgram.process(fingerprint, position, data);
        // Saving program index
        this.nodeProgramIndex[node] = position;
    };
    /**
     * Add the edge data to its program.
     * @private
     * @param edge The edge's graphology ID
     * @param fingerprint A fingerprint used to identity the edge with picking
     * @param position The index where to place the edge in the program
     */
    Sigma.prototype.addEdgeToProgram = function (edge, fingerprint, position) {
        var data = this.edgeDataCache[edge];
        var edgeProgram = this.edgePrograms[data.type];
        if (!edgeProgram)
            throw new Error("Sigma: could not find a suitable program for edge type \"".concat(data.type, "\"!"));
        var extremities = this.graph.extremities(edge), sourceData = this.nodeDataCache[extremities[0]], targetData = this.nodeDataCache[extremities[1]];
        edgeProgram.process(fingerprint, position, sourceData, targetData, data);
        // Saving program index
        this.edgeProgramIndex[edge] = position;
    };
    /**---------------------------------------------------------------------------
     * Public API.
     **---------------------------------------------------------------------------
     */
    /**
     * Method returning the renderer's camera.
     *
     * @return {Camera}
     */
    Sigma.prototype.getCamera = function () {
        return this.camera;
    };
    /**
     * Method setting the renderer's camera.
     *
     * @param  {Camera} camera - New camera.
     * @return {Sigma}
     */
    Sigma.prototype.setCamera = function (camera) {
        this.unbindCameraHandlers();
        this.camera = camera;
        this.bindCameraHandlers();
    };
    /**
     * Method returning the container DOM element.
     *
     * @return {HTMLElement}
     */
    Sigma.prototype.getContainer = function () {
        return this.container;
    };
    /**
     * Method returning the renderer's graph.
     *
     * @return {Graph}
     */
    Sigma.prototype.getGraph = function () {
        return this.graph;
    };
    /**
     * Method used to set the renderer's graph.
     *
     * @return {Graph}
     */
    Sigma.prototype.setGraph = function (graph) {
        if (graph === this.graph)
            return;
        // Unbinding handlers on the current graph
        this.unbindGraphHandlers();
        if (this.checkEdgesEventsFrame !== null) {
            (0, utils_1.cancelFrame)(this.checkEdgesEventsFrame);
            this.checkEdgesEventsFrame = null;
        }
        // Installing new graph
        this.graph = graph;
        // Binding new handlers
        this.bindGraphHandlers();
        // Re-rendering now to avoid discrepancies from now to next frame
        this.refresh();
    };
    /**
     * Method returning the mouse captor.
     *
     * @return {MouseCaptor}
     */
    Sigma.prototype.getMouseCaptor = function () {
        return this.mouseCaptor;
    };
    /**
     * Method returning the touch captor.
     *
     * @return {TouchCaptor}
     */
    Sigma.prototype.getTouchCaptor = function () {
        return this.touchCaptor;
    };
    /**
     * Method returning the current renderer's dimensions.
     *
     * @return {Dimensions}
     */
    Sigma.prototype.getDimensions = function () {
        return { width: this.width, height: this.height };
    };
    /**
     * Method returning the current graph's dimensions.
     *
     * @return {Dimensions}
     */
    Sigma.prototype.getGraphDimensions = function () {
        var extent = this.customBBox || this.nodeExtent;
        return {
            width: extent.x[1] - extent.x[0] || 1,
            height: extent.y[1] - extent.y[0] || 1,
        };
    };
    /**
     * Method used to get all the sigma node attributes.
     * It's usefull for example to get the position of a node
     * and to get values that are set by the nodeReducer
     *
     * @param  {string} key - The node's key.
     * @return {NodeDisplayData | undefined} A copy of the desired node's attribute or undefined if not found
     */
    Sigma.prototype.getNodeDisplayData = function (key) {
        var node = this.nodeDataCache[key];
        return node ? Object.assign({}, node) : undefined;
    };
    /**
     * Method used to get all the sigma edge attributes.
     * It's useful for example to get values that are set by the edgeReducer.
     *
     * @param  {string} key - The edge's key.
     * @return {EdgeDisplayData | undefined} A copy of the desired edge's attribute or undefined if not found
     */
    Sigma.prototype.getEdgeDisplayData = function (key) {
        var edge = this.edgeDataCache[key];
        return edge ? Object.assign({}, edge) : undefined;
    };
    /**
     * Method used to get the set of currently displayed node labels.
     *
     * @return {Set<string>} A set of node keys whose label is displayed.
     */
    Sigma.prototype.getNodeDisplayedLabels = function () {
        return new Set(this.displayedNodeLabels);
    };
    /**
     * Method used to get the set of currently displayed edge labels.
     *
     * @return {Set<string>} A set of edge keys whose label is displayed.
     */
    Sigma.prototype.getEdgeDisplayedLabels = function () {
        return new Set(this.displayedEdgeLabels);
    };
    /**
     * Method returning a copy of the settings collection.
     *
     * @return {Settings} A copy of the settings collection.
     */
    Sigma.prototype.getSettings = function () {
        return __assign({}, this.settings);
    };
    /**
     * Method returning the current value for a given setting key.
     *
     * @param  {string} key - The setting key to get.
     * @return {any} The value attached to this setting key or undefined if not found
     */
    Sigma.prototype.getSetting = function (key) {
        return this.settings[key];
    };
    /**
     * Method setting the value of a given setting key. Note that this will schedule
     * a new render next frame.
     *
     * @param  {string} key - The setting key to set.
     * @param  {any}    value - The value to set.
     * @return {Sigma}
     */
    Sigma.prototype.setSetting = function (key, value) {
        this.settings[key] = value;
        (0, settings_1.validateSettings)(this.settings);
        this.handleSettingsUpdate();
        this.refresh();
        return this;
    };
    /**
     * Method updating the value of a given setting key using the provided function.
     * Note that this will schedule a new render next frame.
     *
     * @param  {string}   key     - The setting key to set.
     * @param  {function} updater - The update function.
     * @return {Sigma}
     */
    Sigma.prototype.updateSetting = function (key, updater) {
        this.settings[key] = updater(this.settings[key]);
        (0, settings_1.validateSettings)(this.settings);
        this.handleSettingsUpdate();
        this.scheduleRefresh();
        return this;
    };
    /**
     * Method used to resize the renderer.
     *
     * @return {Sigma}
     */
    Sigma.prototype.resize = function () {
        var previousWidth = this.width, previousHeight = this.height;
        this.width = this.container.offsetWidth;
        this.height = this.container.offsetHeight;
        this.pixelRatio = (0, utils_1.getPixelRatio)();
        if (this.width === 0) {
            if (this.settings.allowInvalidContainer)
                this.width = 1;
            else
                throw new Error("Sigma: Container has no width. You can set the allowInvalidContainer setting to true to stop seeing this error.");
        }
        if (this.height === 0) {
            if (this.settings.allowInvalidContainer)
                this.height = 1;
            else
                throw new Error("Sigma: Container has no height. You can set the allowInvalidContainer setting to true to stop seeing this error.");
        }
        // If nothing has changed, we can stop right here
        if (previousWidth === this.width && previousHeight === this.height)
            return this;
        this.emit("resize");
        // Sizing dom elements
        for (var id in this.elements) {
            var element = this.elements[id];
            element.style.width = this.width + "px";
            element.style.height = this.height + "px";
        }
        // Sizing canvas contexts
        for (var id in this.canvasContexts) {
            this.elements[id].setAttribute("width", this.width * this.pixelRatio + "px");
            this.elements[id].setAttribute("height", this.height * this.pixelRatio + "px");
            if (this.pixelRatio !== 1)
                this.canvasContexts[id].scale(this.pixelRatio, this.pixelRatio);
        }
        // Sizing WebGL contexts
        for (var id in this.webGLContexts) {
            this.elements[id].setAttribute("width", this.width * this.pixelRatio + "px");
            this.elements[id].setAttribute("height", this.height * this.pixelRatio + "px");
            var gl = this.webGLContexts[id];
            gl.viewport(0, 0, this.width * this.pixelRatio, this.height * this.pixelRatio);
            // Clear picking texture if needed
            if (this.pickingLayers.has(id)) {
                var currentTexture = this.textures[id];
                if (currentTexture)
                    gl.deleteTexture(currentTexture);
            }
        }
        return this;
    };
    /**
     * Method used to clear all the canvases.
     *
     * @return {Sigma}
     */
    Sigma.prototype.clear = function () {
        this.webGLContexts.nodes.clear(this.webGLContexts.nodes.COLOR_BUFFER_BIT);
        this.webGLContexts.edges.clear(this.webGLContexts.edges.COLOR_BUFFER_BIT);
        this.webGLContexts.hoverNodes.clear(this.webGLContexts.hoverNodes.COLOR_BUFFER_BIT);
        this.canvasContexts.labels.clearRect(0, 0, this.width, this.height);
        this.canvasContexts.hovers.clearRect(0, 0, this.width, this.height);
        this.canvasContexts.edgeLabels.clearRect(0, 0, this.width, this.height);
        return this;
    };
    /**
     * Method used to refresh, i.e. force the renderer to reprocess graph
     * data and render, but keep the state.
     * - if a partialGraph is provided, we only reprocess those nodes & edges.
     * - if schedule is TRUE, we schedule a render instead of sync render
     * - if skipIndexation is TRUE, then labelGrid & program indexation are skipped (can be used if you haven't modify x, y, zIndex & size)
     *
     * @return {Sigma}
     */
    Sigma.prototype.refresh = function (opts) {
        var _this = this;
        var _a, _b;
        var skipIndexation = (opts === null || opts === void 0 ? void 0 : opts.skipIndexation) !== undefined ? opts === null || opts === void 0 ? void 0 : opts.skipIndexation : false;
        var schedule = (opts === null || opts === void 0 ? void 0 : opts.schedule) !== undefined ? opts.schedule : false;
        var fullRefresh = !opts || !opts.partialGraph;
        if (fullRefresh) {
            // Re-index graph data
            this.clearEdgeIndices();
            this.clearNodeIndices();
            this.graph.forEachNode(function (node) { return _this.addNode(node); });
            this.graph.forEachEdge(function (edge) { return _this.addEdge(edge); });
        }
        else {
            var nodes = ((_a = opts.partialGraph) === null || _a === void 0 ? void 0 : _a.nodes) || [];
            for (var i = 0, l = (nodes === null || nodes === void 0 ? void 0 : nodes.length) || 0; i < l; i++) {
                var node = nodes[i];
                // Recompute node's data (ie. apply reducer)
                this.updateNode(node);
                // Add node to the program if layout is unchanged.
                // otherwise it will be done in the process function
                if (skipIndexation) {
                    var programIndex = this.nodeProgramIndex[node];
                    if (programIndex === undefined)
                        throw new Error("Sigma: node \"".concat(node, "\" can't be repaint"));
                    this.addNodeToProgram(node, this.nodeIndices[node], programIndex);
                }
            }
            var edges = ((_b = opts === null || opts === void 0 ? void 0 : opts.partialGraph) === null || _b === void 0 ? void 0 : _b.edges) || [];
            for (var i = 0, l = edges.length; i < l; i++) {
                var edge = edges[i];
                // Recompute edge's data (ie. apply reducer)
                this.updateEdge(edge);
                // Add edge to the program
                // otherwise it will be done in the process function
                if (skipIndexation) {
                    var programIndex = this.edgeProgramIndex[edge];
                    if (programIndex === undefined)
                        throw new Error("Sigma: edge \"".concat(edge, "\" can't be repaint"));
                    this.addEdgeToProgram(edge, this.edgeIndices[edge], programIndex);
                }
            }
        }
        // Do we need to call the process function ?
        if (fullRefresh || !skipIndexation)
            this.needToProcess = true;
        if (schedule)
            this.scheduleRender();
        else
            this.render();
        return this;
    };
    /**
     * Method used to schedule a render at the next available frame.
     * This method can be safely called on a same frame because it basically
     * debounces refresh to the next frame.
     *
     * @return {Sigma}
     */
    Sigma.prototype.scheduleRender = function () {
        var _this = this;
        if (!this.renderFrame) {
            this.renderFrame = (0, utils_1.requestFrame)(function () {
                _this.render();
            });
        }
        return this;
    };
    /**
     * Method used to schedule a refresh (i.e. fully reprocess graph data and render)
     * at the next available frame.
     * This method can be safely called on a same frame because it basically
     * debounces refresh to the next frame.
     *
     * @return {Sigma}
     */
    Sigma.prototype.scheduleRefresh = function (opts) {
        return this.refresh(__assign(__assign({}, opts), { schedule: true }));
    };
    /**
     * Method used to (un)zoom, while preserving the position of a viewport point.
     * Used for instance to zoom "on the mouse cursor".
     *
     * @param viewportTarget
     * @param newRatio
     * @return {CameraState}
     */
    Sigma.prototype.getViewportZoomedState = function (viewportTarget, newRatio) {
        var _a = this.camera.getState(), ratio = _a.ratio, angle = _a.angle, x = _a.x, y = _a.y;
        // TODO: handle max zoom
        var ratioDiff = newRatio / ratio;
        var center = {
            x: this.width / 2,
            y: this.height / 2,
        };
        var graphMousePosition = this.viewportToFramedGraph(viewportTarget);
        var graphCenterPosition = this.viewportToFramedGraph(center);
        return {
            angle: angle,
            x: (graphMousePosition.x - graphCenterPosition.x) * (1 - ratioDiff) + x,
            y: (graphMousePosition.y - graphCenterPosition.y) * (1 - ratioDiff) + y,
            ratio: newRatio,
        };
    };
    /**
     * Method returning the abstract rectangle containing the graph according
     * to the camera's state.
     *
     * @return {object} - The view's rectangle.
     */
    Sigma.prototype.viewRectangle = function () {
        // TODO: reduce relative margin?
        var marginX = (0 * this.width) / 8, marginY = (0 * this.height) / 8;
        var p1 = this.viewportToFramedGraph({ x: 0 - marginX, y: 0 - marginY }), p2 = this.viewportToFramedGraph({ x: this.width + marginX, y: 0 - marginY }), h = this.viewportToFramedGraph({ x: 0, y: this.height + marginY });
        return {
            x1: p1.x,
            y1: p1.y,
            x2: p2.x,
            y2: p2.y,
            height: p2.y - h.y,
        };
    };
    /**
     * Method returning the coordinates of a point from the framed graph system to the viewport system. It allows
     * overriding anything that is used to get the translation matrix, or even the matrix itself.
     *
     * Be careful if overriding dimensions, padding or cameraState, as the computation of the matrix is not the lightest
     * of computations.
     */
    Sigma.prototype.framedGraphToViewport = function (coordinates, override) {
        if (override === void 0) { override = {}; }
        var recomputeMatrix = !!override.cameraState || !!override.viewportDimensions || !!override.graphDimensions;
        var matrix = override.matrix
            ? override.matrix
            : recomputeMatrix
                ? (0, utils_1.matrixFromCamera)(override.cameraState || this.camera.getState(), override.viewportDimensions || this.getDimensions(), override.graphDimensions || this.getGraphDimensions(), override.padding || this.getSetting("stagePadding") || 0)
                : this.matrix;
        var viewportPos = (0, matrices_1.multiplyVec2)(matrix, coordinates);
        return {
            x: ((1 + viewportPos.x) * this.width) / 2,
            y: ((1 - viewportPos.y) * this.height) / 2,
        };
    };
    /**
     * Method returning the coordinates of a point from the viewport system to the framed graph system. It allows
     * overriding anything that is used to get the translation matrix, or even the matrix itself.
     *
     * Be careful if overriding dimensions, padding or cameraState, as the computation of the matrix is not the lightest
     * of computations.
     */
    Sigma.prototype.viewportToFramedGraph = function (coordinates, override) {
        if (override === void 0) { override = {}; }
        var recomputeMatrix = !!override.cameraState || !!override.viewportDimensions || !override.graphDimensions;
        var invMatrix = override.matrix
            ? override.matrix
            : recomputeMatrix
                ? (0, utils_1.matrixFromCamera)(override.cameraState || this.camera.getState(), override.viewportDimensions || this.getDimensions(), override.graphDimensions || this.getGraphDimensions(), override.padding || this.getSetting("stagePadding") || 0, true)
                : this.invMatrix;
        var res = (0, matrices_1.multiplyVec2)(invMatrix, {
            x: (coordinates.x / this.width) * 2 - 1,
            y: 1 - (coordinates.y / this.height) * 2,
        });
        if (isNaN(res.x))
            res.x = 0;
        if (isNaN(res.y))
            res.y = 0;
        return res;
    };
    /**
     * Method used to translate a point's coordinates from the viewport system (pixel distance from the top-left of the
     * stage) to the graph system (the reference system of data as they are in the given graph instance).
     *
     * This method accepts an optional camera which can be useful if you need to translate coordinates
     * based on a different view than the one being currently being displayed on screen.
     *
     * @param {Coordinates}                  viewportPoint
     * @param {CoordinateConversionOverride} override
     */
    Sigma.prototype.viewportToGraph = function (viewportPoint, override) {
        if (override === void 0) { override = {}; }
        return this.normalizationFunction.inverse(this.viewportToFramedGraph(viewportPoint, override));
    };
    /**
     * Method used to translate a point's coordinates from the graph system (the reference system of data as they are in
     * the given graph instance) to the viewport system (pixel distance from the top-left of the stage).
     *
     * This method accepts an optional camera which can be useful if you need to translate coordinates
     * based on a different view than the one being currently being displayed on screen.
     *
     * @param {Coordinates}                  graphPoint
     * @param {CoordinateConversionOverride} override
     */
    Sigma.prototype.graphToViewport = function (graphPoint, override) {
        if (override === void 0) { override = {}; }
        return this.framedGraphToViewport(this.normalizationFunction(graphPoint), override);
    };
    /**
     * Method returning the distance multiplier between the graph system and the
     * viewport system.
     */
    Sigma.prototype.getGraphToViewportRatio = function () {
        var graphP1 = { x: 0, y: 0 };
        var graphP2 = { x: 1, y: 1 };
        var graphD = Math.sqrt(Math.pow(graphP1.x - graphP2.x, 2) + Math.pow(graphP1.y - graphP2.y, 2));
        var viewportP1 = this.graphToViewport(graphP1);
        var viewportP2 = this.graphToViewport(graphP2);
        var viewportD = Math.sqrt(Math.pow(viewportP1.x - viewportP2.x, 2) + Math.pow(viewportP1.y - viewportP2.y, 2));
        return viewportD / graphD;
    };
    /**
     * Method returning the graph's bounding box.
     *
     * @return {{ x: Extent, y: Extent }}
     */
    Sigma.prototype.getBBox = function () {
        return this.nodeExtent;
    };
    /**
     * Method returning the graph's custom bounding box, if any.
     *
     * @return {{ x: Extent, y: Extent } | null}
     */
    Sigma.prototype.getCustomBBox = function () {
        return this.customBBox;
    };
    /**
     * Method used to override the graph's bounding box with a custom one. Give `null` as the argument to stop overriding.
     *
     * @return {Sigma}
     */
    Sigma.prototype.setCustomBBox = function (customBBox) {
        this.customBBox = customBBox;
        this.scheduleRender();
        return this;
    };
    /**
     * Method used to shut the container & release event listeners.
     *
     * @return {undefined}
     */
    Sigma.prototype.kill = function () {
        var _a;
        // Emitting "kill" events so that plugins and such can cleanup
        this.emit("kill");
        // Releasing events
        this.removeAllListeners();
        // Releasing camera handlers
        this.unbindCameraHandlers();
        // Releasing DOM events & captors
        window.removeEventListener("resize", this.activeListeners.handleResize);
        this.mouseCaptor.kill();
        this.touchCaptor.kill();
        // Releasing graph handlers
        this.unbindGraphHandlers();
        // Releasing cache & state
        this.clearIndices();
        this.clearState();
        this.nodeDataCache = {};
        this.edgeDataCache = {};
        this.highlightedNodes.clear();
        // Clearing frames
        if (this.renderFrame) {
            (0, utils_1.cancelFrame)(this.renderFrame);
            this.renderFrame = null;
        }
        if (this.renderHighlightedNodesFrame) {
            (0, utils_1.cancelFrame)(this.renderHighlightedNodesFrame);
            this.renderHighlightedNodesFrame = null;
        }
        // Destroying WebGL contexts
        for (var id in this.webGLContexts) {
            var context = this.webGLContexts[id];
            (_a = context.getExtension("WEBGL_lose_context")) === null || _a === void 0 ? void 0 : _a.loseContext();
        }
        // Destroying canvases
        var container = this.container;
        while (container.firstChild)
            container.removeChild(container.firstChild);
        // Destroying remaining collections
        this.canvasContexts = {};
        this.webGLContexts = {};
        this.elements = {};
        this.nodePrograms = {};
        this.nodeHoverPrograms = {};
        this.edgePrograms = {};
    };
    /**
     * Method used to scale the given size according to the camera's ratio, i.e.
     * zooming state.
     *
     * @param  {number?} size -        The size to scale (node size, edge thickness etc.).
     * @param  {number?} cameraRatio - A camera ratio (defaults to the actual camera ratio).
     * @return {number}              - The scaled size.
     */
    Sigma.prototype.scaleSize = function (size, cameraRatio) {
        if (size === void 0) { size = 1; }
        if (cameraRatio === void 0) { cameraRatio = this.camera.ratio; }
        return ((size / this.settings.zoomToSizeRatioFunction(cameraRatio)) *
            (this.getSetting("itemSizesReference") === "positions" ? cameraRatio * this.graphToViewportRatio : 1));
    };
    /**
     * Method that returns the collection of all used canvases.
     * At the moment, the instantiated canvases are the following, and in the
     * following order in the DOM:
     * - `edges`
     * - `nodes`
     * - `edgeLabels`
     * - `labels`
     * - `hovers`
     * - `hoverNodes`
     * - `mouse`
     *
     * @return {PlainObject<HTMLCanvasElement>} - The collection of canvases.
     */
    Sigma.prototype.getCanvases = function () {
        return __assign({}, this.elements);
    };
    return Sigma;
}(types_1.TypedEventEmitter));
exports.default = Sigma;
