import { Host, h } from "@stencil/core";
import * as d3 from "d3";
import dayjs from "dayjs";
import { cloneDeep, filter, first, isEmpty, isObject, keys, last, sortBy, values, } from "lodash-es";
import { applyErrorMessage, clearErrorMessage, } from "../../utils/error-message";
import { getStore } from "../../store";
import { watchStoreLocale } from "../../utils/locale-state";
import { getStoreUnavailableError } from "../../utils/store-error";
import { getLocale } from "../../utils/i18n";
import { translations } from "../../utils/translations";
const TOTAL_WINDOW_MINUTES = 60;
const FULL_WINDOW_WIDTH = 640;
const MIN_AUTO_VISIBLE_MINUTES = 10;
const FULL_SIZE_POINT_RADIUS = 5;
function createXAxisTickConfigs() {
    const hourMinuteSecondTickFormatter = d3.timeFormat("%H:%M:%S");
    const hourMinuteTickFormatter = d3.timeFormat("%H:%M");
    return [
        ...[15, 30].map((step) => ({
            interval: d3.timeSecond,
            step,
            format: (date) => hourMinuteSecondTickFormatter(date),
        })),
        ...[1, 2, 5, 10, 15, 30].map((step) => ({
            interval: d3.timeMinute,
            step,
            format: (date) => hourMinuteTickFormatter(date),
        })),
        {
            interval: d3.timeHour,
            step: 1,
            format: (date) => hourMinuteTickFormatter(date),
        },
    ];
}
export class LsRecentStops {
    constructor() {
        /**
         * Number of minutes to show in the initial view. Defaults to 60 (the full data window).
         * Set a smaller value to zoom in on the most recent data at load time.
         */
        this.initialVisibleMinutes = 60;
        /**
         * Minimum number of minutes that must remain visible when zooming in. Defaults to 5.
         */
        this.minVisibleMinutes = 5;
        this.store = null;
        this.dateTimeFormatter = this.createDateTimeFormatter();
        this.MARGIN = { BOTTOM: 28, LEFT: 28, RIGHT: 20, TOP: 28 };
        this.MIN_CHART_HEIGHT = 160;
        this.X_AXIS_EDGE_TICK_THRESHOLD = 10;
        this.X_AXIS_MIN_TICK_SPACING = 64;
        this.X_AXIS_TICK_CONFIGS = createXAxisTickConfigs();
        // key 501 is current (open doors), 515 is current (regardless of door state), 514 is last passed
        this.SENSOR_IDS = [501, 514, 515];
        this.focusCircleId = "focusCircle";
        this.focusLineXId = "focusLineX";
        this.focusLineYId = "focusLineY";
        this.focusTextContainerId = "focusTextContainer";
        this.focusTextId = "focusText";
        this.width = 0;
        this.chartHeight = 0;
        this.clipPathId = `chart-clip-${Math.random().toString(36).slice(2, 9)}`;
        this.currentXScale = undefined;
        this.referenceXScale = undefined;
        this.currentYScale = undefined;
        this.graph = undefined;
        this.yAxisSvg = undefined;
        this.chartSvgElement = null;
        this.zoomBehavior = null;
        this.currentZoomTransform = d3.zoomIdentity;
        this.zoomRedrawFrame = null;
        this.resizeRedrawFrame = null;
        this.lastDrawnWidth = 0;
        this.resizeObserver = null;
        this.observedContainerWidth = 0;
        this.observedContainerHeight = 0;
        this.timePoints = [];
        this.maxFloors = 0;
        this.isComponentMounted = false;
        this.storeUnsubscribes = [];
        /**
         * Centralized error state for tracking component errors
         */
        this.errorState = {
            hasError: false,
            messageKey: undefined,
            type: "",
        };
        /**
         * Component loading state
         */
        this.isLoading = true;
        this.appendSensorUpdatesToTimePoints = (data, timestamp) => {
            if (isEmpty(data))
                return;
            this.updateErrorState();
            for (let i = 0; i < keys(data).length; i++) {
                const key = parseInt(keys(data)[i], 10);
                const value = parseInt(values(data)[i], 10);
                if (key === 591) {
                    this.maxFloors = (value || 0) + 1;
                }
                if (!Number.isNaN(value) && this.SENSOR_IDS.includes(key)) {
                    const momentTimestamp = dayjs(timestamp[key]);
                    const newTimePoint = {
                        x: momentTimestamp.toDate(),
                        y: value + 1,
                        id: `${key}-${timestamp[key]}`,
                        sensor: key,
                        fake: false,
                    };
                    const updatedTimePoints = [...this.timePoints, newTimePoint];
                    this.timePoints = updatedTimePoints;
                }
            }
        };
        this.formatTimestamp = (timestamp) => {
            return this.dateTimeFormatter.format(dayjs(timestamp).toDate());
        };
        this.clearChart = () => {
            this.detachZoomAndPointerEvents();
            this.clearZoomRedrawFrame();
            d3.select(this.yAxisCanvas).html("");
            d3.select(this.chartCanvas).html("");
            this.chartSvgElement = null;
            this.zoomBehavior = null;
            this.graph = undefined;
            this.yAxisSvg = undefined;
        };
        this.handleWheelGesture = (event) => {
            if (this.chartSvgElement === null || this.zoomBehavior === null)
                return;
            const svgSelection = d3.select(this.chartSvgElement);
            // Trackpad two-finger horizontal swipe should scrub left/right.
            if (this.isMostlyHorizontalWheelGesture(event)) {
                event.preventDefault();
                const zoomScale = Math.max(this.currentZoomTransform.k, 0.0001);
                const worldDeltaX = -event.deltaX / zoomScale;
                svgSelection.call(this.zoomBehavior.translateBy, worldDeltaX, 0);
            }
        };
        this.drawChart = () => {
            var _a, _b, _c, _d, _e;
            this.clearChart();
            const componentWidth = ((_a = this.chartContainerElement) === null || _a === void 0 ? void 0 : _a.clientWidth) ||
                ((_b = this.containerElement) === null || _b === void 0 ? void 0 : _b.clientWidth) ||
                this.element.clientWidth ||
                1;
            this.width = Math.max(1, componentWidth - this.MARGIN.LEFT - this.MARGIN.RIGHT);
            const didResize = this.width !== this.lastDrawnWidth;
            this.lastDrawnWidth = this.width;
            const chartWidth = this.width + this.MARGIN.RIGHT;
            const yAxisWidth = this.MARGIN.LEFT;
            const calculatedHeight = componentWidth >= 800
                ? this.width / 4 - this.MARGIN.BOTTOM - this.MARGIN.TOP
                : this.width / 3 - this.MARGIN.BOTTOM - this.MARGIN.TOP;
            const height = Math.max(this.MIN_CHART_HEIGHT, calculatedHeight);
            this.chartHeight = height;
            const svgHeight = height + this.MARGIN.BOTTOM + this.MARGIN.TOP;
            this.referenceXScale = d3.scaleTime().range([0, this.width]);
            const now = dayjs();
            const start = now.subtract(TOTAL_WINDOW_MINUTES, "minute");
            this.referenceXScale.domain([start.valueOf(), now.valueOf()]);
            if (didResize) {
                const minScale = this.getMinScale();
                const maxScale = this.getMaxScale();
                const clampedScale = Math.min(maxScale, Math.max(minScale, (_d = (_c = this.currentZoomTransform) === null || _c === void 0 ? void 0 : _c.k) !== null && _d !== void 0 ? _d : 1));
                this.currentZoomTransform = this.getRightAlignedTransform(clampedScale);
            }
            else {
                this.currentZoomTransform = this.getAppliedZoomTransform();
            }
            this.currentXScale = this.currentZoomTransform.rescaleX(this.referenceXScale);
            const pointRadius = this.getPointRadius();
            this.currentYScale = d3.scaleLinear().range([height, 0]);
            this.xAxis = this.createXAxis(this.currentXScale);
            this.yAxis = d3
                .axisLeft(this.currentYScale)
                .tickValues(d3.range(1, Math.max(this.maxFloors, 1) + 1))
                .tickSize(3)
                .tickPadding(3)
                .tickFormat(d3.format("d"));
            // Fixed y-axis SVG
            this.yAxisSvg = d3
                .select(this.yAxisCanvas)
                .append("svg")
                .attr("viewBox", `0 0 ${yAxisWidth} ${svgHeight}`)
                .attr("width", yAxisWidth)
                .attr("height", svgHeight)
                .append("g")
                .attr("transform", `translate(${yAxisWidth}, ${this.MARGIN.TOP})`);
            this.yAxisSvg.append("g").attr("class", "axis y_axis").call(this.yAxis);
            // Chart SVG — zoom/pan is attached to this element
            const chartSvgSelection = d3
                .select(this.chartCanvas)
                .append("svg")
                .attr("viewBox", `0 0 ${chartWidth} ${svgHeight}`)
                .attr("width", chartWidth)
                .attr("height", svgHeight);
            // Clip path to prevent data overflowing axes during zoom/pan
            chartSvgSelection
                .append("defs")
                .append("clipPath")
                .attr("id", this.clipPathId)
                .append("rect")
                .attr("x", 0)
                .attr("y", 0)
                .attr("width", this.width)
                .attr("height", height);
            this.updateClipPathBounds(pointRadius, height);
            this.chartSvgElement = chartSvgSelection.node();
            const svg = chartSvgSelection
                .append("g")
                .attr("transform", `translate(0, ${this.MARGIN.TOP})`);
            svg
                .append("g")
                .attr("class", "axis x_axis")
                .attr("transform", `translate(0,${height})`)
                .call(this.xAxis);
            const dataSets = !isEmpty(this.historicalPointsResponse)
                ? this.historicalPointsResponse
                : [];
            const historicalTimePoints = [];
            for (const dataSet of dataSets) {
                const sensorId = parseInt(dataSet.sensor.id, 10);
                if (sensorId === 591) {
                    this.maxFloors = (parseInt((_e = last(dataSet.rows)) === null || _e === void 0 ? void 0 : _e.val, 10) || 0) + 1;
                }
                for (const row of dataSet.rows) {
                    const value = parseInt(row.val, 10);
                    if (!Number.isNaN(value) && this.SENSOR_IDS.includes(sensorId)) {
                        const newTimePoint = {
                            x: dayjs(row.ts).toDate(),
                            y: value + 1,
                            id: `${sensorId}-${row.ts}`,
                            sensor: sensorId,
                            fake: false,
                        };
                        historicalTimePoints.push(newTimePoint);
                    }
                }
            }
            const mergedTimePoints = Array.from(new Map([...historicalTimePoints, ...this.timePoints].map((timePoint) => [
                timePoint.id,
                timePoint,
            ])).values());
            mergedTimePoints.sort((a, b) => (dayjs(a.x).isBefore(dayjs(b.x)) ? -1 : 1));
            this.timePoints = mergedTimePoints;
            // Data group clipped to chart area
            this.graph = svg.append("g").attr("clip-path", `url(#${this.clipPathId})`);
            const focus = svg.append("g").style("display", "none");
            focus
                .append("line")
                .attr("id", this.focusLineXId)
                .attr("class", "focusLine");
            focus
                .append("line")
                .attr("id", this.focusLineYId)
                .attr("class", "focusLine");
            focus
                .append("circle")
                .attr("id", this.focusCircleId)
                .attr("r", pointRadius)
                .attr("class", "circle focusCircle");
            const textContainer = focus
                .append("g")
                .attr("id", this.focusTextContainerId)
                .attr("class", "focus_text_container");
            textContainer
                .append("rect")
                .attr("width", 180)
                .attr("height", 20)
                .attr("x", -90)
                .attr("y", -20)
                .attr("rx", 5)
                .attr("ry", 5);
            textContainer
                .append("text")
                .attr("y", -10)
                .attr("id", this.focusTextId)
                .attr("text-anchor", "middle")
                .attr("dy", ".35em");
            const [floorExtend, datesExtend] = this.updateDataRange();
            const bisectDate = d3.bisector((timepoint) => timepoint.x).left;
            svg
                .append("rect")
                .attr("class", "overlay")
                .attr("width", this.width)
                .attr("height", height)
                .on("mouseout", () => focus.style("display", "none"))
                .on("mousemove", (event) => {
                const mouse = d3.pointer(event);
                const mouseDate = this.currentXScale.invert(mouse[0]);
                const clonedTimePoints = cloneDeep(this.timePoints);
                clonedTimePoints.sort((a, b) => a.x.valueOf() - b.x.valueOf());
                const bisectIndex = bisectDate(clonedTimePoints, mouseDate);
                const timePoint0 = clonedTimePoints[bisectIndex - 1];
                const timePoint1 = clonedTimePoints[bisectIndex];
                if (isObject(timePoint0) && isObject(timePoint1)) {
                    const closestTimePoint = mouseDate.getTime() - timePoint0.x.getTime() >
                        timePoint1.x.getTime() - mouseDate.getTime()
                        ? timePoint1
                        : timePoint0;
                    const xPosition = this.currentXScale(closestTimePoint.x);
                    const yPosition = this.currentYScale(closestTimePoint.y);
                    focus
                        .select(`#${this.focusCircleId}`)
                        .attr("cx", xPosition)
                        .attr("cy", yPosition);
                    focus
                        .select(`#${this.focusLineXId}`)
                        .attr("x1", xPosition)
                        .attr("y1", this.currentYScale(floorExtend[0]))
                        .attr("x2", xPosition)
                        .attr("y2", this.currentYScale(floorExtend[1]));
                    focus
                        .select(`#${this.focusLineYId}`)
                        .attr("x1", this.currentXScale(datesExtend[0]))
                        .attr("y1", yPosition)
                        .attr("x2", this.currentXScale(datesExtend[1]))
                        .attr("y2", yPosition);
                    focus
                        .select(`#${this.focusTextContainerId}`)
                        .attr("transform", `translate(${xPosition + 2},${yPosition})`)
                        .select(`#${this.focusTextId}`)
                        .text(this.formatTimestamp(closestTimePoint.x));
                    focus.style("display", null);
                }
            })
                .node();
            // Attach zoom and pointer event handlers after the SVG is fully built
            this.zoomBehavior = this.buildZoomBehavior(height);
            this.attachZoomAndPointerEvents(this.chartSvgElement, true);
            if (this.errorState.hasError) {
                this.detachZoomAndPointerEvents();
                this.setOverlayPointerEvents("none");
            }
        };
        this.updateDataRange = () => {
            var _a, _b;
            const now = dayjs();
            const start = now.subtract(TOTAL_WINDOW_MINUTES, "minute");
            let filteredTimePoints = filter(this.timePoints, (timePoint) => dayjs(timePoint.x).isAfter(start) && !timePoint.fake);
            if (filteredTimePoints.length === 0) {
                for (const point of this.timePoints) {
                    if (point.sensor === 515) {
                        const currentFloorPoint = cloneDeep(point);
                        currentFloorPoint.x = now.toDate();
                        currentFloorPoint.id = `${currentFloorPoint.y}-${now.toDate()}`;
                        currentFloorPoint.fake = true;
                        filteredTimePoints = [...filteredTimePoints, currentFloorPoint];
                    }
                }
            }
            const clonedTimePoints = cloneDeep(filteredTimePoints);
            clonedTimePoints.sort((a, b) => a.x.valueOf() - b.x.valueOf());
            const fakeValueLast = cloneDeep(last(clonedTimePoints));
            if (fakeValueLast !== undefined) {
                fakeValueLast.x = now.toDate();
                fakeValueLast.id = `${fakeValueLast.y}-${now.toDate()}`;
                fakeValueLast.fake = true;
                this.timePoints = [...filteredTimePoints, fakeValueLast];
            }
            const fakeValueFirst = cloneDeep(first(clonedTimePoints));
            if (fakeValueFirst !== undefined) {
                fakeValueFirst.x = start.toDate();
                fakeValueFirst.id = `${fakeValueFirst.y}-${start.toDate()}`;
                fakeValueFirst.fake = true;
                this.timePoints = [...this.timePoints, fakeValueFirst];
            }
            const maxFloors = Math.max(this.maxFloors, 1);
            const floorExtent = this.getFloorExtent(maxFloors);
            const datesExtend = d3.extent(this.timePoints, (tp) => tp.x);
            datesExtend[1] = now.toDate();
            // Update reference scale domain — this is the "full" 60-minute domain
            (_a = this.referenceXScale) === null || _a === void 0 ? void 0 : _a.domain([start.valueOf(), now.valueOf()]);
            // Derive current (zoomed) x scale from current transform applied to the updated reference
            this.currentXScale = this.currentZoomTransform.rescaleX(this.referenceXScale);
            (_b = this.currentYScale) === null || _b === void 0 ? void 0 : _b.domain(floorExtent);
            this.yAxis.tickValues(d3.range(1, maxFloors + 1));
            if (this.yAxisSvg !== undefined) {
                d3.select(this.element.shadowRoot.querySelector(".y_axis")).call(this.yAxis);
            }
            this.redrawAxesAndData();
            return [floorExtent, datesExtend];
        };
    }
    createDateTimeFormatter(localeInput) {
        const locale = getLocale(localeInput);
        return new Intl.DateTimeFormat(translations[locale].intlLocale, {
            day: "2-digit",
            month: "2-digit",
            year: "numeric",
            hour: "2-digit",
            minute: "2-digit",
            second: "2-digit",
            hour12: false,
        });
    }
    /**
     * Updates the error state based on current component conditions
     */
    updateErrorState() {
        var _a;
        const previousErrorState = this.errorState.hasError;
        if (this.store === null) {
            this.errorState = getStoreUnavailableError((_a = this.store) === null || _a === void 0 ? void 0 : _a.state.locale);
            return;
        }
        if (this.store.state.error.hasError) {
            this.errorState = {
                hasError: true,
                messageKey: this.store.state.error.messageKey,
                type: this.store.state.error.type,
            };
            if (this.containerElement !== undefined) {
                // Keep the chart mounted and overlay the shared error UI on top.
                applyErrorMessage(this.containerElement, this.getErrorMessageForDisplay());
            }
            this.clearTimer();
            this.detachZoomAndPointerEvents();
            this.setOverlayPointerEvents("none");
            return;
        }
        // No error detected
        this.errorState = {
            hasError: false,
            messageKey: undefined,
            type: "",
        };
        if (this.containerElement !== undefined) {
            clearErrorMessage(this.containerElement);
        }
        if (previousErrorState &&
            !this.errorState.hasError &&
            this.chartSvgElement !== null &&
            this.zoomBehavior !== null) {
            this.attachZoomAndPointerEvents(this.chartSvgElement);
        }
        // If we just recovered from an error state, restart the timer
        if (previousErrorState &&
            !this.errorState.hasError &&
            this.timer === null) {
            this.startTimer();
        }
    }
    startTimer() {
        // Clear any existing timer first
        this.clearTimer();
        // Initiate the timer that updates the graph
        this.timer = window.setInterval(() => {
            if (!this.isComponentMounted) {
                this.clearTimer();
                return;
            }
            this.updateDataRange();
        }, 2000);
    }
    clearTimer() {
        window.clearInterval(this.timer);
        this.timer = null;
    }
    detachZoomAndPointerEvents() {
        if (this.chartSvgElement !== null) {
            d3.select(this.chartSvgElement).on(".zoom", null);
        }
        if (this.chartCanvas !== undefined) {
            this.chartCanvas.removeEventListener("wheel", this.handleWheelGesture);
        }
    }
    setOverlayPointerEvents(pointerEvents) {
        var _a;
        const overlay = (_a = this.element.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector(".overlay");
        if (overlay !== null && overlay !== undefined) {
            d3.select(overlay).style("pointer-events", pointerEvents);
        }
    }
    clearZoomRedrawFrame() {
        if (this.zoomRedrawFrame !== null) {
            window.cancelAnimationFrame(this.zoomRedrawFrame);
            this.zoomRedrawFrame = null;
        }
    }
    scheduleZoomRedraw() {
        if (this.zoomRedrawFrame !== null)
            return;
        this.zoomRedrawFrame = window.requestAnimationFrame(() => {
            this.zoomRedrawFrame = null;
            if (!this.isComponentMounted || this.graph === undefined) {
                return;
            }
            this.redrawAxesAndData();
        });
    }
    getMaxScale() {
        const { minVisibleMinutes } = this.visibleMinutes;
        return Math.max(this.getMinScale(), TOTAL_WINDOW_MINUTES / minVisibleMinutes);
    }
    getMaxVisibleMinutesForWidth() {
        if (this.width >= FULL_WINDOW_WIDTH) {
            return TOTAL_WINDOW_MINUTES;
        }
        const scaledVisibleMinutes = (TOTAL_WINDOW_MINUTES * Math.max(this.width, MIN_AUTO_VISIBLE_MINUTES)) /
            FULL_WINDOW_WIDTH;
        const increasedDownsizingVisibleMinutes = TOTAL_WINDOW_MINUTES -
            (TOTAL_WINDOW_MINUTES - scaledVisibleMinutes) * 1.25;
        return Math.min(TOTAL_WINDOW_MINUTES, Math.max(MIN_AUTO_VISIBLE_MINUTES, Math.round(increasedDownsizingVisibleMinutes)));
    }
    getMinScale() {
        return TOTAL_WINDOW_MINUTES / this.getMaxVisibleMinutesForWidth();
    }
    getRightAlignedTransform(scale) {
        return d3.zoomIdentity.translate(this.width * (1 - scale), 0).scale(scale);
    }
    get visibleMinutes() {
        const minVisibleMinutes = Math.min(TOTAL_WINDOW_MINUTES, Math.max(1, Number(this.minVisibleMinutes) || 1));
        const initialVisibleMinutes = Math.min(TOTAL_WINDOW_MINUTES, this.getMaxVisibleMinutesForWidth(), Math.max(minVisibleMinutes, Number(this.initialVisibleMinutes) || TOTAL_WINDOW_MINUTES));
        return { initialVisibleMinutes, minVisibleMinutes };
    }
    createXAxis(scale) {
        const tickConfig = this.getXAxisTickConfig(scale);
        const tickInterval = tickConfig.interval.every(tickConfig.step);
        const tickValues = tickInterval !== null
            ? scale.ticks(tickInterval)
            : scale.ticks(Math.max(2, this.getXAxisTargetTickCount()));
        return d3
            .axisBottom(scale)
            .tickValues(this.filterXAxisTickValuesNearChartEdges(scale, tickValues))
            .tickFormat(tickConfig.format);
    }
    getXAxisTargetTickCount() {
        const plotWidth = Math.max(1, this.width);
        return Math.max(2, Math.floor(plotWidth / this.X_AXIS_MIN_TICK_SPACING));
    }
    getXAxisTickConfig(scale) {
        var _a;
        const [domainStart, domainEnd] = scale.domain();
        const visibleDurationMs = Math.max(1, Math.abs(domainEnd.valueOf() - domainStart.valueOf()));
        const targetTickCount = this.getXAxisTargetTickCount();
        return ((_a = this.X_AXIS_TICK_CONFIGS.find(({ interval, step }) => {
            const intervalDurationMs = interval.offset(new Date(0), step).valueOf();
            return visibleDurationMs / intervalDurationMs <= targetTickCount;
        })) !== null && _a !== void 0 ? _a : this.X_AXIS_TICK_CONFIGS[this.X_AXIS_TICK_CONFIGS.length - 1]);
    }
    filterXAxisTickValuesNearChartEdges(scale, tickValues) {
        const edgeStart = this.X_AXIS_EDGE_TICK_THRESHOLD;
        const edgeEnd = this.width - this.X_AXIS_EDGE_TICK_THRESHOLD;
        return tickValues.filter((tickValue) => {
            const tickX = scale(tickValue);
            return tickX > edgeStart && tickX < edgeEnd;
        });
    }
    isMostlyHorizontalWheelGesture(event) {
        const absX = Math.abs(event.deltaX);
        const absY = Math.abs(event.deltaY);
        return absX > 0.5 && absX >= absY * 0.35;
    }
    shouldHandleZoomEvent(event) {
        if (event instanceof WheelEvent) {
            return !this.isMostlyHorizontalWheelGesture(event);
        }
        if (event.type.startsWith("touch")) {
            return true;
        }
        if (event instanceof MouseEvent) {
            return event.button === 0;
        }
        return true;
    }
    getWheelZoomDelta(event) {
        const baseDelta = -event.deltaY *
            (event.deltaMode === WheelEvent.DOM_DELTA_LINE
                ? 0.05
                : event.deltaMode === WheelEvent.DOM_DELTA_PAGE
                    ? 1
                    : 0.002);
        return baseDelta * (event.ctrlKey ? 10 : 1.75);
    }
    buildZoomBehavior(height) {
        const minScale = this.getMinScale();
        const maxScale = this.getMaxScale();
        return d3
            .zoom()
            .extent([
            [0, 0],
            [this.width, height],
        ])
            .scaleExtent([minScale, maxScale])
            .translateExtent([
            [0, 0],
            [this.width, height],
        ])
            .wheelDelta((event) => this.getWheelZoomDelta(event))
            .filter((event) => this.shouldHandleZoomEvent(event))
            .on("zoom", (event) => {
            this.currentZoomTransform = event.transform;
            this.currentXScale = event.transform.rescaleX(this.referenceXScale);
            this.scheduleZoomRedraw();
        });
    }
    getAppliedZoomTransform() {
        const minScale = this.getMinScale();
        const hasExistingZoom = this.currentZoomTransform.k !== minScale ||
            this.currentZoomTransform.x !== 0 ||
            this.currentZoomTransform.y !== 0;
        if (hasExistingZoom) {
            if (this.currentZoomTransform.k <= minScale + 0.0001) {
                return this.getRightAlignedTransform(minScale);
            }
            return this.currentZoomTransform;
        }
        const { initialVisibleMinutes: visibleMinutes } = this.visibleMinutes;
        const zoomLevel = Math.max(TOTAL_WINDOW_MINUTES / visibleMinutes, minScale);
        if (zoomLevel <= minScale && visibleMinutes >= TOTAL_WINDOW_MINUTES) {
            return this.getRightAlignedTransform(minScale);
        }
        if (visibleMinutes >= TOTAL_WINDOW_MINUTES && minScale <= 1) {
            return this.currentZoomTransform;
        }
        const translateX = this.width * (1 - zoomLevel);
        return d3.zoomIdentity.translate(translateX, 0).scale(zoomLevel);
    }
    getVisiblePoints() {
        if (this.currentXScale === undefined)
            return [];
        const [domainStart, domainEnd] = this.currentXScale
            .domain()
            .map((date) => date.valueOf());
        return this.timePoints.filter((timePoint) => {
            if (timePoint.fake)
                return false;
            const timestamp = timePoint.x.valueOf();
            return timestamp >= domainStart && timestamp <= domainEnd;
        });
    }
    getVisiblePointColumns(visiblePoints) {
        return Array.from(new Set(visiblePoints.map((timePoint) => Math.round(this.currentXScale(timePoint.x))))).sort((a, b) => a - b);
    }
    getRepresentativeGap(pointColumns) {
        var _a;
        if (pointColumns.length <= 1)
            return this.width;
        const gaps = [];
        for (let index = 1; index < pointColumns.length; index += 1) {
            const gap = pointColumns[index] - pointColumns[index - 1];
            if (gap > 0) {
                gaps.push(gap);
            }
        }
        if (gaps.length === 0)
            return 0;
        gaps.sort((a, b) => a - b);
        // Bias towards a more typical gap instead of the tighter tail.
        return (_a = gaps[Math.floor(gaps.length * 0.5)]) !== null && _a !== void 0 ? _a : gaps[0];
    }
    getPointRadius() {
        const visiblePoints = this.getVisiblePoints();
        const visiblePointCount = visiblePoints.length;
        const rowHeight = this.chartHeight / Math.max(this.maxFloors, 1);
        const heightBasedRadius = rowHeight * 0.34;
        const widthBasedRadius = this.width / 150;
        const minScale = this.getMinScale();
        const isZoomedIn = this.currentZoomTransform.k > minScale + 0.0001;
        const maxRadiusForWidth = this.width <= 420
            ? isZoomedIn
                ? FULL_SIZE_POINT_RADIUS
                : 4.5
            : this.width <= 520
                ? 5
                : 5.5;
        const minRadiusForWidth = this.width <= 420 ? 2.5 : 3;
        const baseRadius = Math.min(maxRadiusForWidth, Math.max(4.5, heightBasedRadius, widthBasedRadius));
        const isWideChart = this.width > FULL_WINDOW_WIDTH;
        if (isWideChart) {
            return FULL_SIZE_POINT_RADIUS;
        }
        if (visiblePointCount <= 1)
            return baseRadius;
        const visiblePointColumns = this.getVisiblePointColumns(visiblePoints);
        const visibleColumnCount = Math.max(visiblePointColumns.length, 1);
        const averageColumnGap = this.width / visibleColumnCount;
        const representativeGap = this.getRepresentativeGap(visiblePointColumns);
        if (this.width <= 420) {
            const effectiveGap = isZoomedIn
                ? Math.max(averageColumnGap, representativeGap * 0.95)
                : Math.min(representativeGap, averageColumnGap * 1.15);
            if (effectiveGap >= (isZoomedIn ? 8 : 10))
                return baseRadius;
            if (effectiveGap >= (isZoomedIn ? 6.5 : 8))
                return Math.min(baseRadius, 4);
            if (effectiveGap >= (isZoomedIn ? 5 : 6.5))
                return Math.min(baseRadius, 3.5);
            if (effectiveGap >= 5)
                return Math.min(baseRadius, 3);
            return minRadiusForWidth;
        }
        const effectiveGap = Math.max(averageColumnGap, representativeGap * 0.9);
        if (effectiveGap >= 12)
            return baseRadius;
        if (effectiveGap >= 9.5)
            return Math.min(baseRadius, 4.5);
        if (effectiveGap >= 7)
            return Math.min(baseRadius, 4);
        if (effectiveGap >= 5.5)
            return Math.min(baseRadius, 3.5);
        return minRadiusForWidth;
    }
    getFloorExtent(maxFloors) {
        const clampedMaxFloors = Math.max(maxFloors, 1);
        return [1, clampedMaxFloors];
    }
    updateClipPathBounds(pointRadius, height) {
        var _a;
        d3.select((_a = this.element.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector(`#${this.clipPathId} rect`))
            .attr("x", -pointRadius - 2)
            .attr("y", -pointRadius - 2)
            .attr("width", this.width + (pointRadius + 2) * 2)
            .attr("height", height + (pointRadius + 2) * 2);
    }
    attachZoomAndPointerEvents(svgEl, applyInitialTransform = false) {
        this.detachZoomAndPointerEvents();
        const svgSelection = d3.select(svgEl);
        svgSelection.call(this.zoomBehavior);
        this.setOverlayPointerEvents("all");
        if (this.chartCanvas !== undefined) {
            this.chartCanvas.addEventListener("wheel", this.handleWheelGesture, {
                passive: false,
            });
        }
        if (applyInitialTransform) {
            const targetTransform = this.getAppliedZoomTransform();
            this.currentZoomTransform = targetTransform;
            this.currentXScale = targetTransform.rescaleX(this.referenceXScale);
            if (targetTransform.k !== 1 ||
                targetTransform.x !== 0 ||
                targetTransform.y !== 0) {
                svgSelection.call(this.zoomBehavior.transform, targetTransform);
            }
        }
    }
    scheduleResizeRedraw() {
        if (this.isLoading)
            return;
        if (this.resizeRedrawFrame !== null)
            return;
        this.resizeRedrawFrame = window.requestAnimationFrame(() => {
            this.resizeRedrawFrame = null;
            if (!this.isComponentMounted || this.isLoading) {
                return;
            }
            this.drawChart();
        });
    }
    updateObservedContainerSize(nextWidth, nextHeight) {
        const didChange = nextWidth !== this.observedContainerWidth ||
            nextHeight !== this.observedContainerHeight;
        this.observedContainerWidth = nextWidth;
        this.observedContainerHeight = nextHeight;
        return didChange;
    }
    clearResizeRedrawFrame() {
        if (this.resizeRedrawFrame !== null) {
            window.cancelAnimationFrame(this.resizeRedrawFrame);
            this.resizeRedrawFrame = null;
        }
    }
    redrawAxesAndData() {
        const height = this.chartHeight;
        const pointRadius = this.getPointRadius();
        this.updateClipPathBounds(pointRadius, height);
        // Update x-axis to reflect current zoomed scale
        this.xAxis = this.createXAxis(this.currentXScale);
        d3.select(this.element.shadowRoot.querySelector(".x_axis")).call(this.xAxis);
        this.graph.selectAll(".line").remove();
        for (let i = 1; i <= this.maxFloors; i++) {
            this.graph
                .append("line")
                .attr("y1", this.currentYScale(i))
                .attr("y2", this.currentYScale(i))
                .attr("x1", 0.5)
                .attr("x2", this.width)
                .attr("class", "line");
        }
        this.graph.selectAll(".path").remove();
        this.graph
            .append("path")
            .datum(sortBy(this.timePoints, (t) => t.x.getTime()).map((tp) => [
            tp.x.getTime(),
            tp.y,
        ]))
            .attr("fill", "none")
            .attr("class", "path")
            .attr("d", d3
            .line()
            .x((d) => this.currentXScale(d[0]))
            .y((d) => this.currentYScale(d[1])));
        this.graph.selectAll(".point").remove();
        this.graph
            .selectAll(".circle")
            .data(sortBy(this.timePoints, "sensor").reverse())
            .enter()
            .append("circle")
            .attr("cx", (tp) => this.currentXScale(tp.x))
            .attr("cy", (tp) => this.currentYScale(tp.y))
            .attr("r", pointRadius)
            .attr("class", (tp) => {
            if (tp.fake)
                return "fake point";
            const sensor = parseInt(`${tp.sensor}`, 10);
            return `${sensor === 514 || sensor === 515 ? "passed" : "stop"} point`;
        });
        d3.select(this.element.shadowRoot.querySelector(`#${this.focusCircleId}`)).attr("r", pointRadius);
        // Keep the overlay rect on top
        d3.select(this.element.shadowRoot.querySelector(".overlay")).raise();
        // Resize overlay to match current chart width
        d3.select(this.element.shadowRoot.querySelector(".overlay"))
            .attr("width", this.width)
            .attr("height", height);
    }
    getErrorMessageForDisplay() {
        var _a;
        if (this.store === null)
            return "";
        const errorKey = (_a = this.errorState.messageKey) !== null && _a !== void 0 ? _a : "CONSOLE_INFO";
        return this.store.t(`errors:${errorKey}`);
    }
    updateErrorMessageElement() {
        if (this.containerElement === undefined || this.store === null)
            return;
        if (!this.errorState.hasError) {
            clearErrorMessage(this.containerElement);
            return;
        }
        applyErrorMessage(this.containerElement, this.getErrorMessageForDisplay());
    }
    async componentWillLoad() {
        this.isComponentMounted = true;
        this.store = await getStore(this.idKey);
        watchStoreLocale(this, this.store, (locale) => {
            this.dateTimeFormatter = this.createDateTimeFormatter(locale);
            this.updateErrorMessageElement();
        });
        this.updateErrorState();
        // Subscribe to error changes before first render
        if (this.store !== null) {
            this.storeUnsubscribes.push(this.store.onChange("error", () => {
                this.updateErrorState();
                this.updateErrorMessageElement();
            }));
        }
    }
    async componentDidLoad() {
        var _a, _b;
        if (this.store === null) {
            this.isLoading = false;
            this.updateErrorState();
            return;
        }
        this.historicalPointsResponse = this.store.state.historicalSensorData;
        this.drawChart();
        this.isLoading = false;
        this.updateErrorState();
        if (typeof ResizeObserver !== "undefined") {
            this.observedContainerWidth =
                ((_a = this.chartContainerElement) === null || _a === void 0 ? void 0 : _a.clientWidth) || 0;
            this.observedContainerHeight =
                ((_b = this.chartContainerElement) === null || _b === void 0 ? void 0 : _b.clientHeight) || 0;
            this.resizeObserver = new ResizeObserver((entries) => {
                const entry = entries[0];
                if (entry === undefined)
                    return;
                const nextWidth = Math.round(entry.contentRect.width);
                const nextHeight = Math.round(entry.contentRect.height);
                if (!this.updateObservedContainerSize(nextWidth, nextHeight)) {
                    return;
                }
                this.scheduleResizeRedraw();
            });
            if (this.chartContainerElement !== undefined) {
                this.resizeObserver.observe(this.chartContainerElement);
            }
        }
        this.storeUnsubscribes.push(this.store.onChange("historicalSensorData", (sensorValues) => {
            this.historicalPointsResponse = sensorValues;
            this.drawChart();
        }));
        this.storeUnsubscribes.push(this.store.onChange("currentSensorData", (sensorData) => {
            const values = sensorData.values;
            const timestamps = sensorData.updated;
            this.appendSensorUpdatesToTimePoints(values, timestamps);
            this.updateDataRange();
        }));
        this.startTimer();
    }
    render() {
        var _a, _b;
        return (h(Host, { key: 'd8b7ec452d7772e7a6782991c92a6befa3ccf887' }, h("div", { key: 'f87052ebdee62f30745169268d77af7c76ea7170', class: { container: true, "has-error": this.errorState.hasError }, ref: (el) => {
                this.containerElement = el;
            } }, h("div", { key: 'bbf8b0808cdb92cac67a6f444bc113bd4ddfb273', class: "chart-container", ref: (el) => {
                this.chartContainerElement = el;
            } }, h("div", { key: '7e5f8020ce0135186993621ea345417fc62ba24c', class: "y-axis-container", ref: (el) => {
                this.yAxisCanvas = el;
            } }), h("div", { key: 'a2b9a775e5a66409a1ac76419450639ae796a87b', class: "chart", ref: (el) => {
                this.chartCanvas = el;
            } })), this.isLoading && (h("div", { key: 'cc29ac1b2c27f46667248cefbb75f7a0949fcb86', class: "loading" }, (_b = (_a = this.store) === null || _a === void 0 ? void 0 : _a.t("ui:general.loading")) !== null && _b !== void 0 ? _b : "Loading...")), h("slot", { key: '3293dff17f975f8109981caf697f163d2ea25eb0' }))));
    }
    disconnectedCallback() {
        var _a;
        this.isComponentMounted = false;
        this.clearTimer();
        this.detachZoomAndPointerEvents();
        this.clearZoomRedrawFrame();
        this.clearResizeRedrawFrame();
        (_a = this.resizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
        this.resizeObserver = null;
        this.storeUnsubscribes.forEach((unsubscribe) => unsubscribe());
        this.storeUnsubscribes = [];
        if (this.containerElement !== undefined) {
            clearErrorMessage(this.containerElement);
        }
    }
    static get is() { return "ls-recent-stops"; }
    static get encapsulation() { return "shadow"; }
    static get originalStyleUrls() {
        return {
            "$": ["ls-recent-stops.css"]
        };
    }
    static get styleUrls() {
        return {
            "$": ["ls-recent-stops.css"]
        };
    }
    static get properties() {
        return {
            "idKey": {
                "type": "string",
                "attribute": "id-key",
                "mutable": false,
                "complexType": {
                    "original": "string",
                    "resolved": "string",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "The unique key that is used to identify store data."
                },
                "getter": false,
                "setter": false,
                "reflect": false
            },
            "initialVisibleMinutes": {
                "type": "number",
                "attribute": "initial-visible-minutes",
                "mutable": false,
                "complexType": {
                    "original": "number",
                    "resolved": "number",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Number of minutes to show in the initial view. Defaults to 60 (the full data window).\nSet a smaller value to zoom in on the most recent data at load time."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "60"
            },
            "minVisibleMinutes": {
                "type": "number",
                "attribute": "min-visible-minutes",
                "mutable": false,
                "complexType": {
                    "original": "number",
                    "resolved": "number",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Minimum number of minutes that must remain visible when zooming in. Defaults to 5."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "5"
            }
        };
    }
    static get states() {
        return {
            "errorState": {},
            "isLoading": {}
        };
    }
    static get elementRef() { return "element"; }
}
//# sourceMappingURL=ls-recent-stops.js.map
