import _ from "lodash";
import { createIterable } from "@deck.gl/core";
import { TextLayer } from "@deck.gl/layers";
import createKdTree from "static-kdtree";
const DEFAULT_PROPS = {
    mergeRadius: 100,
    mergeLabels: true,
    getClusterText: (texts) => {
        if (texts.length > 1) {
            // Elide the text if there are multiple labels in the cluster
            return `${texts[0]} (+${texts.length - 1})`;
        }
        return texts[0];
    },
};
/**
 * MergedTextLayer is a subclass of TextLayer that merges text labels
 * that are near each other.
 */
export class MergedTextLayer extends TextLayer {
    shouldUpdateState(params) {
        if (params.changeFlags.propsOrDataChanged) {
            return true;
        }
        return super.shouldUpdateState(params);
    }
    updateState(params) {
        super.updateState(params);
        if (params.changeFlags.propsOrDataChanged) {
            this.updateLabelPositions();
            this.updateLabelClusters();
            this.updateInstanceState();
            this.setState({
                getText: _.bind(this.getText, this),
            });
        }
    }
    getSubLayerProps(sublayerProps) {
        var _a, _b;
        const allUpdateTriggers = (_b = (_a = sublayerProps === null || sublayerProps === void 0 ? void 0 : sublayerProps.updateTriggers) === null || _a === void 0 ? void 0 : _a["all"]) !== null && _b !== void 0 ? _b : [];
        const newProps = Object.assign(Object.assign({}, sublayerProps), { getColor: _.bind(this.getColor, this), getFillColor: _.bind(this.getBackgroundColor, this), getLineColor: _.bind(this.getBorderColor, this), updateTriggers: Object.assign(Object.assign({}, sublayerProps === null || sublayerProps === void 0 ? void 0 : sublayerProps.updateTriggers), { all: [
                    ...allUpdateTriggers,
                    this.props.mergeRadius,
                    this.props.mergeLabels,
                    this.props.getPosition,
                ] }) });
        return super.getSubLayerProps(newProps);
    }
    updateLabelPositions() {
        var _a;
        const { data, getText } = this.props;
        let { getPosition } = this.getSubLayerProps(Object.assign({}, this.props));
        if (_.isUndefined(getPosition)) {
            getPosition = this.props.getPosition;
        }
        const labelPositions = new Map();
        const positionToText = new Map();
        const { iterable, objectInfo } = createIterable(data);
        for (const object of iterable) {
            const text = getText(object, objectInfo);
            const position = typeof getPosition === "function"
                ? getPosition(object, objectInfo)
                : getPosition;
            if (_.isUndefined(position)) {
                continue;
            }
            labelPositions.set(text, position);
            if (positionToText.has(position)) {
                (_a = positionToText.get(position)) === null || _a === void 0 ? void 0 : _a.push(text);
            }
            else {
                positionToText.set(position, [text]);
            }
        }
        this.setState({
            labelPositions,
            positionToText,
        });
    }
    /**
     * `TextLayer` ignores `getText` from derived layers, so we need
     * to override the state here
     */
    updateInstanceState() {
        const { data } = this.props;
        let numInstances = 0;
        const startIndices = [0];
        const { iterable, objectInfo } = createIterable(data);
        for (const object of iterable) {
            const text = Array.from(this.getText(object, objectInfo));
            numInstances += text.length;
            startIndices.push(numInstances);
        }
        this.setState({
            numInstances,
            startIndices,
        });
    }
    updateLabelClusters() {
        var _a, _b;
        const positions = [...this.state.labelPositions.values()];
        if (positions.length === 0) {
            return;
        }
        const positionTree = createKdTree(positions);
        const clusters = new Map();
        for (const position of positions) {
            const neighbors = [];
            const visitor = (id) => {
                neighbors.push(id);
            };
            positionTree.rnn(position, (_a = this.props.mergeRadius) !== null && _a !== void 0 ? _a : 10, visitor);
            const text = this.state.positionToText.get(position);
            if (_.isUndefined(text)) {
                continue;
            }
            neighbors.sort();
            for (const id of neighbors) {
                const text = this.state.positionToText.get(positions[id]);
                if (_.isUndefined(text)) {
                    continue;
                }
                const cluster = (_b = clusters.get(position)) !== null && _b !== void 0 ? _b : [];
                clusters.set(position, [...cluster, ...text]);
            }
        }
        this.setState({
            clusters,
        });
        positionTree.dispose();
    }
    getText(object, objectInfo) {
        const { getText, mergeLabels, getClusterText } = this.props;
        const text = getText(object, objectInfo);
        if (!mergeLabels) {
            return text;
        }
        const position = this.state.labelPositions.get(text);
        if (_.isUndefined(position)) {
            return text;
        }
        const cluster = this.state.clusters.get(position);
        if (_.isUndefined(cluster)) {
            return text;
        }
        if (text === cluster[0]) {
            return getClusterText(cluster);
        }
        // Hide the text if it is not the first label in the cluster
        return "";
    }
    getBorderColor(object, objectInfo) {
        const { getText, getBorderColor, mergeLabels } = this.props;
        const text = getText(object, objectInfo);
        const borderColor = typeof getBorderColor === "function"
            ? getBorderColor(object, objectInfo)
            : getBorderColor;
        if (!mergeLabels) {
            return borderColor;
        }
        const position = this.state.labelPositions.get(text);
        if (_.isUndefined(position)) {
            return [255, 100, 100, 255];
        }
        const cluster = this.state.clusters.get(position);
        if (_.isUndefined(cluster)) {
            return [255, 100, 100, 255];
        }
        if (cluster[0] === text) {
            return borderColor;
        }
        return [borderColor[0], borderColor[1], borderColor[2], 0];
    }
    getColor(object, objectInfo) {
        const { getText, getColor, mergeLabels } = this.props;
        const text = getText(object, objectInfo);
        const color = typeof getColor === "function"
            ? getColor(object, objectInfo)
            : getColor;
        if (!mergeLabels) {
            return color;
        }
        const position = this.state.labelPositions.get(text);
        if (_.isUndefined(position)) {
            return [255, 100, 100, 255];
        }
        const cluster = this.state.clusters.get(position);
        if (_.isUndefined(cluster)) {
            return [255, 100, 100, 255];
        }
        if (cluster[0] === text) {
            return color;
        }
        // Hide the text if it is not the first label in the cluster, in which case it will be
        // elided
        return [0, 0, 0, 0];
    }
    getBackgroundColor(object, objectInfo) {
        const { getText, getBackgroundColor, mergeLabels } = this.props;
        const text = getText(object, objectInfo);
        const backgroundColor = typeof getBackgroundColor === "function"
            ? getBackgroundColor(object, objectInfo)
            : getBackgroundColor;
        if (!mergeLabels) {
            return backgroundColor;
        }
        const position = this.state.labelPositions.get(text);
        if (_.isUndefined(position)) {
            return [255, 100, 100, 255];
        }
        const cluster = this.state.clusters.get(position);
        if (_.isUndefined(cluster)) {
            return [0, 0, 0, 0];
        }
        if (cluster[0] === text) {
            return backgroundColor;
        }
        return [backgroundColor[0], backgroundColor[1], backgroundColor[2], 0];
    }
}
MergedTextLayer.defaultProps = DEFAULT_PROPS;
MergedTextLayer.layerName = "MergedTextLayer";
//# sourceMappingURL=mergedTextLayer.js.map