WME Live Alerts

Display Live Map alerts in WME.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==UserScript==
// @name         WME Live Alerts
// @author       Kieran Davies
// @description  Display Live Map alerts in WME.
// @match        https://*.waze.com/*/editor*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=waze.com
// @version      0.2.1
// @license      MIT
// @grant        none
// @namespace    https://greasyfork.org/users/1577571
// ==/UserScript==

(function () {
  "use strict";

  const LIVEMAP_API_URL = "https://www.waze.com/live-map/api/georss";
  const ASSET_BASE_URL =
    "https://raw.githubusercontent.com/kierandavies06/wme-scripts/refs/heads/main/assets/images/";
  const DEFAULT_ENV = "row";
  const MAP_MOVE_FETCH_DEBOUNCE_MS = 800;
  const MIN_VISIBLE_ZOOM = 14;
  const ALERTS_LAYER_NAME = "wme-live-alerts-layer";
  const ALERTS_LAYER_CHECKBOX_NAME = "Live Alerts";

  const SCRIPT_ID = "wme-live-alerts";
  const SCRIPT_NAME = "WME Live Alerts";
  const SCRIPT_TAB_LABEL = "Live Alerts";
  const USERSCRIPTS_PANEL_ROOT_ID = `${SCRIPT_ID}-userscripts-root`;
  const USERSCRIPTS_PANEL_STYLE =
    "border:1px solid var(--separator_default, rgba(0,0,0,0.15));border-radius:6px;padding:8px;margin-top:8px;";
  const USERSCRIPTS_DESCRIPTION_STYLE =
    "font-size:11px;opacity:0.85;margin-bottom:8px;";
  const USERSCRIPTS_SECTION_STYLE =
    "margin-top:10px;padding-top:8px;border-top:1px solid var(--separator_default, rgba(0,0,0,0.15));display:flex;flex-direction:column;gap:6px;";
  const USERSCRIPTS_BUTTON_STYLE =
    "height:auto;min-height:unset;line-height:1.25;white-space:normal;text-align:left;padding:6px 10px;";
  const USERSCRIPTS_STATUS_STYLE = "margin-top:8px;opacity:0.85;";
  const USERSCRIPTS_LIST_STYLE =
    "margin-top:8px;max-height:220px;overflow:auto;font-size:12px;opacity:0.9;";
  const LAYER_VISIBILITY_STORAGE_KEY = "wme-live-alerts:layer-visible";
  const HAZARD_FILTER_STORAGE_KEY =
    "wme-live-alerts:visible-hazard-subtypes";

  const ALERT_DEFINITION_ROWS = {
    CHIT_CHAT: ["chit-chat.svg", "Chit-chat"],
    POLICE: ["police.svg", "Police"],
    POLICE_VISIBLE: ["police.svg", "Police"],
    POLICE_HIDDEN: ["police.svg", "Hidden police"],
    POLICE_HIDING: ["police.svg", "Hidden police"],
    ACCIDENT: ["accident-major.svg", "Accident"],
    ACCIDENT_MINOR: ["accident-minor.svg", "Minor accident"],
    ACCIDENT_MAJOR: ["accident-major.svg", "Major accident"],
    JAM: ["jam-level-2.svg", "Traffic jam"],
    JAM_LIGHT_TRAFFIC: ["jam-level-1.svg", "Light traffic"],
    JAM_MODERATE_TRAFFIC: ["jam-level-2.svg", "Moderate traffic"],
    JAM_HEAVY_TRAFFIC: ["jam-level-3.svg", "Heavy traffic"],
    JAM_STAND_STILL_TRAFFIC: ["jam-level-4.svg", "Standstill traffic"],
    TRAFFIC_INFO: ["hazard.svg", "Traffic info"],
    HAZARD: ["hazard.svg", "Hazard"],
    HAZARD_ON_ROAD: ["hazard.svg", "Hazard on road"],
    HAZARD_ON_SHOULDER: ["hazard.svg", "Hazard on shoulder"],
    HAZARD_WEATHER: ["hazard.svg", "Weather hazard"],
    HAZARD_ON_ROAD_OBJECT: ["object-on-road.svg", "Object on road"],
    HAZARD_ON_ROAD_POT_HOLE: ["pothole.svg", "Pothole"],
    HAZARD_ON_ROAD_ROAD_KILL: ["roadkill.svg", "Roadkill"],
    HAZARD_ON_SHOULDER_CAR_STOPPED: ["vehicle-stopped.svg", "Vehicle stopped"],
    HAZARD_ON_ROAD_CAR_STOPPED: ["vehicle-stopped.svg", "Vehicle stopped"],
    HAZARD_ON_SHOULDER_ANIMALS: ["animals.svg", "Animals on shoulder"],
    HAZARD_ON_SHOULDER_MISSING_SIGN: ["missing-sign.svg", "Missing sign"],
    HAZARD_WEATHER_FOG: ["fog.svg", "Fog"],
    HAZARD_WEATHER_HAIL: ["hail.svg", "Hail"],
    HAZARD_WEATHER_HEAVY_RAIN: ["flood.svg", "Heavy rain"],
    HAZARD_WEATHER_HEAVY_SNOW: ["unplowed-road.svg", "Heavy snow"],
    HAZARD_WEATHER_FLOOD: ["flood.svg", "Flooding"],
    HAZARD_WEATHER_MONSOON: ["flood.svg", "Monsoon"],
    HAZARD_WEATHER_TORNADO: ["hazard.svg", "Tornado"],
    HAZARD_WEATHER_HEAT_WAVE: ["hazard.svg", "Heat wave"],
    HAZARD_WEATHER_HURRICANE: ["hazard.svg", "Hurricane"],
    HAZARD_WEATHER_FREEZING_RAIN: ["ice-on-road.svg", "Freezing rain"],
    HAZARD_ON_ROAD_LANE_CLOSED: ["closure.svg", "Lane closed"],
    HAZARD_ON_ROAD_OIL: ["hazard.svg", "Oil on road"],
    HAZARD_ON_ROAD_ICE: ["ice-on-road.svg", "Ice on road"],
    HAZARD_ON_ROAD_CONSTRUCTION: ["construction.svg", "Construction"],
    HAZARD_ON_ROAD_EMERGENCY_VEHICLE: ["hazard.svg", "Emergency vehicle"],
    HAZARD_ON_ROAD_TRAFFIC_LIGHT_FAULT: [
      "broken-light.svg",
      "Broken traffic light",
    ],
    LANE_CLOSURE_BLOCKED_LANES: ["closure.svg", "Blocked lanes"],
    LANE_CLOSURE_LEFT_LANE: ["closure.svg", "Left lane closed"],
    LANE_CLOSURE_RIGHT_LANE: ["closure.svg", "Right lane closed"],
    LANE_CLOSURE_CENTER_LANE: ["closure.svg", "Center lane closed"],
    ROAD_CLOSED: ["closure.svg", "Road closed"],
    ROAD_CLOSED_HAZARD: ["closure.svg", "Road closed (hazard)"],
    ROAD_CLOSED_CONSTRUCTION: ["closure.svg", "Road closed (construction)"],
    ROAD_CLOSED_EVENT: ["closure.svg", "Road closed (event)"],
    PARKED_ON: ["vehicle-stopped.svg", "Parked on road"],
    PARKED_OFF: ["vehicle-stopped.svg", "Parked off road"],
    MISC: ["hazard.svg", "Misc"],
    CONSTRUCTION: ["construction.svg", "Construction"],
    PARKING: ["vehicle-stopped.svg", "Parking"],
    DYNAMIC: ["hazard.svg", "Dynamic"],
    CAMERA: ["police-mobile-camera.svg", "Camera"],
    PARKED: ["vehicle-stopped.svg", "Parked vehicle"],
    SYSTEM_ROAD_CLOSED: ["closure.svg", "System road closed"],
    SOS: ["hazard.svg", "SOS"],
    NO_SUBTYPE: ["hazard.svg", "No subtype"],
    UNKKNOWN: ["hazard.svg", "Unknown"],
  };
  /** @type {Record<string, { spriteUrl: string, label: string }>} */
  const ALERT_DEFINITIONS = Object.entries(ALERT_DEFINITION_ROWS).reduce(
    /** @param {Record<string, { spriteUrl: string, label: string }>} definitions */
    (definitions, [key, [spriteUrl, label]]) => {
      definitions[key] = { spriteUrl, label };
      return definitions;
    },
    {},
  );
  const FILTER_GROUP_COLLAPSE_STORAGE_KEY =
    "wme-live-alerts:collapsed-filter-groups";
  const FILTERABLE_ALERT_CODES = ["HAZARD", "ROAD_CLOSED", "SYSTEM_ROAD_CLOSED"];
  const HAZARD_FILTER_OPTIONS = Object.entries(ALERT_DEFINITIONS)
    .filter(
      ([code]) => FILTERABLE_ALERT_CODES.includes(code)
        || code.startsWith("HAZARD_")
        || code.startsWith("ROAD_CLOSED_"),
    )
    .map(([code, { label }]) => ({ code, label }))
    .sort((left, right) => left.label.localeCompare(right.label));
  const DEFAULT_VISIBLE_HAZARD_CODES = HAZARD_FILTER_OPTIONS.map(
    ({ code }) => code,
  );
  const DEFAULT_VISIBLE_HAZARD_CODE_SET = new Set(DEFAULT_VISIBLE_HAZARD_CODES);
  const HAZARD_FILTER_GROUPS = (() => {
    const groupedDefinitions = [
      {
        key: "general",
        label: "Hazards",
        match: (/** @type {string} */ code) => code === "HAZARD",
      },
      {
        key: "road",
        label: "On road",
        match: (/** @type {string} */ code) => code.startsWith("HAZARD_ON_ROAD"),
      },
      {
        key: "shoulder",
        label: "On shoulder",
        match: (/** @type {string} */ code) => code.startsWith("HAZARD_ON_SHOULDER"),
      },
      {
        key: "weather",
        label: "Weather",
        match: (/** @type {string} */ code) => code.startsWith("HAZARD_WEATHER"),
      },
      {
        key: "closures",
        label: "Closures",
        match: (/** @type {string} */ code) => code === "ROAD_CLOSED"
          || code === "SYSTEM_ROAD_CLOSED"
          || code.startsWith("ROAD_CLOSED_"),
      },
    ];
    const remainingCodes = new Set(DEFAULT_VISIBLE_HAZARD_CODES);
    const groups = groupedDefinitions
      .map(({ key, label, match }) => {
        const options = HAZARD_FILTER_OPTIONS.filter(({ code }) => match(code));
        options.forEach(({ code }) => remainingCodes.delete(code));
        return { key, label, options };
      })
      .filter(({ options }) => options.length);

    if (remainingCodes.size) {
      groups.push({
        key: "other",
        label: "Other",
        options: HAZARD_FILTER_OPTIONS.filter(({ code }) =>
          remainingCodes.has(code),
        ),
      });
    }

    return groups;
  })();
  const HAZARD_FILTER_GROUP_KEY_SET = new Set(
    HAZARD_FILTER_GROUPS.map(({ key }) => key),
  );

  /**
     * @type {null}
     */
  let lastFetchedBounds = null;
  /**
     * @type {any[]}
     */
  let cachedAlerts = [];
  /**
     * @type {AbortController | null}
     */
  let activeRequestController = null;
  let popupAlertsByFeatureId = new Map();
  let popupPageByFeatureId = new Map();
  /**
     * @type {HTMLDivElement | null}
     */
  let popupElement = null;
  /**
     * @type {null}
     */
  let hoveredFeatureId = null;
  /**
     * @type {null}
     */
  let pinnedFeatureId = null;
  let ignoreNextMapClickClose = false;
  let isAlertsLayerVisible = true;
  let visibleHazardCodes = new Set(getStoredVisibleHazardCodes());
  let collapsedFilterGroupKeys = new Set(getStoredCollapsedFilterGroupKeys());
  /**
     * @type {Element | null}
     */
  let userscriptsPanelElement = null;
  let userscriptsPanelActiveTab = "stats";
  /**
     * @type {null}
     */
  let sdkInstance = null;

  /**
     * @param {{ (): void; (arg0: any): any; }} fn
     * @param {number | undefined} delayMs
     */
  function debounce(fn, delayMs) {
    /**
       * @type {number | undefined}
       */
    let timeoutId;
    return (/** @type {any} */ ...args) => {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => fn(...args), delayMs);
    };
  }

  /**
     * @param {HTMLElement} element
     * @param {string} styleText
     */
  function applyStyleText(element, styleText) {
    if (element && typeof styleText === "string") {
      element.style.cssText = styleText;
    }
  }

  /**
     * @param {{ container: any; content: any; element: any; root: any; tabContent: any; tabElement: any; tabPane: any; }} result
     */
  function extractSidebarContainerFromRegistrationResult(result) {
    if (result instanceof HTMLElement) {
      return result;
    }

    const directCandidates = [
      result?.container,
      result?.content,
      result?.element,
      result?.root,
      result?.tabContent,
      result?.tabElement,
      result?.tabPane,
    ];

    for (const candidate of directCandidates) {
      if (candidate instanceof HTMLElement) {
        return candidate;
      }
    }

    return null;
  }

  /**
     * @param {HTMLElement | null} containerElement
     */
  function ensureUserscriptsContentRoot(containerElement) {
    if (!(containerElement instanceof HTMLElement)) {
      return null;
    }

    const existingRoot = containerElement.querySelector(
      `#${USERSCRIPTS_PANEL_ROOT_ID}`,
    );
    const root =
      existingRoot instanceof HTMLElement
        ? existingRoot
        : document.createElement("div");

    if (!(existingRoot instanceof HTMLElement)) {
      root.id = USERSCRIPTS_PANEL_ROOT_ID;
      containerElement.appendChild(root);
    }

    root.style.paddingLeft = "15px";
    root.style.paddingRight = "15px";
    return root;
  }

  /**
     * @param {{ Events: { on: (arg0: { eventName: any; eventHandler: any; }) => void; }; }} sdk
     * @param {string} eventName
     * @param {{ ({ layerName, featureId }: { layerName: any; featureId: any; }): void; ({ layerName, featureId }: { layerName: any; featureId: any; }): void; ({ layerName, featureId }: { layerName: any; featureId: any; }): void; (): void; ({ name, checked }: { name: any; checked: any; }): void; (): void; }} eventHandler
     */
  function onEvent(sdk, eventName, eventHandler) {
    sdk.Events.on({ eventName, eventHandler });
  }

  function getStoredLayerVisibility() {
    try {
      const storedValue = window.localStorage.getItem(
        LAYER_VISIBILITY_STORAGE_KEY,
      );
      return storedValue === null ? true : storedValue === "true";
    } catch {
      return true;
    }
  }

  /**
     * @param {any} isVisible
     */
  function setStoredLayerVisibility(isVisible) {
    try {
      window.localStorage.setItem(
        LAYER_VISIBILITY_STORAGE_KEY,
        String(Boolean(isVisible)),
      );
    } catch {}
  }

  function getStoredVisibleHazardCodes() {
    try {
      const storedValue = window.localStorage.getItem(HAZARD_FILTER_STORAGE_KEY);
      if (!storedValue) {
        return [...DEFAULT_VISIBLE_HAZARD_CODES];
      }

      const parsedValue = JSON.parse(storedValue);
      if (!Array.isArray(parsedValue)) {
        return [...DEFAULT_VISIBLE_HAZARD_CODES];
      }

      const normalizedCodes = parsedValue.filter((code) =>
        DEFAULT_VISIBLE_HAZARD_CODE_SET.has(code),
      );
      return normalizedCodes;
    } catch {
      return [...DEFAULT_VISIBLE_HAZARD_CODES];
    }
  }

  /**
     * @param {any[]} codes
     */
  function setStoredVisibleHazardCodes(codes) {
    try {
      const normalizedCodes = (Array.isArray(codes) ? codes : []).filter(
        (code) => DEFAULT_VISIBLE_HAZARD_CODE_SET.has(code),
      );
      window.localStorage.setItem(
        HAZARD_FILTER_STORAGE_KEY,
        JSON.stringify(normalizedCodes),
      );
    } catch {}
  }

  /**
     * @param {any[]} codes
     */
  function setVisibleHazardCodes(codes) {
    const normalizedCodes = (Array.isArray(codes) ? codes : []).filter((code) =>
      DEFAULT_VISIBLE_HAZARD_CODE_SET.has(code),
    );
    visibleHazardCodes = new Set(normalizedCodes);
    setStoredVisibleHazardCodes(normalizedCodes);
  }

  function getStoredCollapsedFilterGroupKeys() {
    try {
      const storedValue = window.localStorage.getItem(
        FILTER_GROUP_COLLAPSE_STORAGE_KEY,
      );
      if (!storedValue) {
        return [];
      }

      const parsedValue = JSON.parse(storedValue);
      return Array.isArray(parsedValue)
        ? parsedValue.filter((key) => HAZARD_FILTER_GROUP_KEY_SET.has(key))
        : [];
    } catch {
      return [];
    }
  }

  /**
     * @param {any[]} keys
     */
  function setCollapsedFilterGroupKeys(keys) {
    const normalizedKeys = (Array.isArray(keys) ? keys : []).filter((key) =>
      HAZARD_FILTER_GROUP_KEY_SET.has(key),
    );
    collapsedFilterGroupKeys = new Set(normalizedKeys);

    try {
      window.localStorage.setItem(
        FILTER_GROUP_COLLAPSE_STORAGE_KEY,
        JSON.stringify(normalizedKeys),
      );
    } catch {}
  }

  /**
     * @param {{ type?: string; subtype?: string; }} alert
     */
  function getAlertFilterCode(alert) {
    if (!alert || typeof alert !== "object") {
      return null;
    }

    if (
      typeof alert.subtype === "string"
      && DEFAULT_VISIBLE_HAZARD_CODE_SET.has(alert.subtype)
    ) {
      return alert.subtype;
    }

    if (
      typeof alert.type === "string"
      && DEFAULT_VISIBLE_HAZARD_CODE_SET.has(alert.type)
    ) {
      return alert.type;
    }

    return null;
  }

  /**
     * @param {{ type?: string; subtype?: string; }} alert
     */
  function isAlertVisibleByFilters(alert) {
    const filterCode = getAlertFilterCode(alert);
    return filterCode ? visibleHazardCodes.has(filterCode) : true;
  }

  /**
     * @param {any} alerts
     */
  function getFilteredAlerts(alerts) {
    return (Array.isArray(alerts) ? alerts : []).filter((alert) =>
      isAlertVisibleByFilters(alert),
    );
  }

  function refreshAlertVisibility(statusText = "") {
    if (sdkInstance) {
      renderAlertsOnLayer(sdkInstance, cachedAlerts);
    }

    updateUserscriptsPanelStats(
      cachedAlerts,
      statusText ||
        (sdkInstance && !isAtVisibleZoom(sdkInstance)
          ? `Zoom in to ${MIN_VISIBLE_ZOOM}+ to load alerts.`
          : ""),
    );
  }

  /**
     * @param {any[]} alerts
     */
  function getTopSubtypeStats(alerts, limit = 5) {
    const counts = new Map();
    (Array.isArray(alerts) ? alerts : []).forEach((alert) => {
      const code = alert?.subtype || alert?.type;
      const label =
        translateAlertCode(code) ||
        translateAlertCode(alert?.type) ||
        "Unknown";
      counts.set(label, (counts.get(label) || 0) + 1);
    });

    return Array.from(counts.entries())
      .sort(
        (left, right) => right[1] - left[1] || left[0].localeCompare(right[0]),
      )
      .slice(0, limit)
      .map(([label, count]) => ({ label, count }));
  }

  /**
     * @param {any[]} alerts
     */
  function updateUserscriptsPanelStats(alerts, statusText = "") {
    if (!userscriptsPanelElement) {
      return;
    }

    const visibleAlerts = getFilteredAlerts(alerts);
    const totalAlerts = visibleAlerts.length;
    const topStats = getTopSubtypeStats(visibleAlerts);
    const enabledHazardCount = HAZARD_FILTER_OPTIONS.filter(({ code }) =>
      visibleHazardCodes.has(code),
    ).length;
    const statsHtml = topStats.length
      ? `<ol style="margin:6px 0 0 18px;padding:0;">${topStats.map(({ label, count }) => `<li><strong>${count}</strong> ${escapeHtml(label)}</li>`).join("")}</ol>`
      : "No alerts match the current filters.";
    const isConfigTab = userscriptsPanelActiveTab === "config";
    const tabButtonStyle = (/** @type {boolean} */ isActive) =>
      `${USERSCRIPTS_BUTTON_STYLE}${isActive ? "font-weight:600;" : ""}`;
    const hazardFilterHtml = HAZARD_FILTER_GROUPS.map(
      ({ key, label, options }) => {
        const enabledCount = options.filter(({ code }) =>
          visibleHazardCodes.has(code),
        ).length;
        const optionHtml = options
          .map(
            ({ code, label: optionLabel }) => `
              <label style="font-size:11px;white-space:nowrap;align-self:flex-start;cursor:pointer;"><input type="checkbox" data-live-alerts-hazard-filter="${code}" ${visibleHazardCodes.has(code) ? "checked" : ""} /> ${escapeHtml(optionLabel)}</label>
            `,
          )
          .join("");

        return `
          <details data-live-alerts-filter-group-details="${key}" ${collapsedFilterGroupKeys.has(key) ? "" : "open"} style="border:1px solid var(--separator_default, rgba(0,0,0,0.15));border-radius:6px;padding:8px;">
            <summary style="cursor:pointer;display:flex;justify-content:space-between;align-items:center;gap:8px;">
              <span style="font-size:11px;font-weight:600;opacity:0.9;">${escapeHtml(label)}</span>
              <span style="font-size:11px;opacity:0.8;">${enabledCount}/${options.length}</span>
            </summary>
            <div style="display:flex;gap:6px;margin:8px 0 6px;align-items:stretch;">
              <button type="button" data-live-alerts-hazard-group="${key}" data-live-alerts-hazard-group-action="all" style="${tabButtonStyle(false)}">All</button>
              <button type="button" data-live-alerts-hazard-group="${key}" data-live-alerts-hazard-group-action="none" style="${tabButtonStyle(false)}">None</button>
            </div>
            <div style="display:flex;flex-direction:column;gap:6px;align-items:flex-start;">${optionHtml}</div>
          </details>
        `;
      },
    ).join("");

    applyStyleText(userscriptsPanelElement, USERSCRIPTS_PANEL_STYLE);
    userscriptsPanelElement.innerHTML = `
            <div style="font-weight:600;margin-bottom:6px;">${SCRIPT_NAME}</div>
            <div style="${USERSCRIPTS_DESCRIPTION_STYLE}">Display Live Map alerts in WME.</div>
            <div style="${USERSCRIPTS_DESCRIPTION_STYLE}"><b>Note:</b> Use the filters below to show or hide specific hazards and road closures.</div>
            <div style="display:flex;flex-direction:column;gap:8px;align-items:stretch;">
                <button type="button" data-live-alerts-tab="config" style="${tabButtonStyle(isConfigTab)}">Config</button>
                <button type="button" data-live-alerts-tab="stats" style="${tabButtonStyle(!isConfigTab)}">Stats</button>
            </div>
            <div data-live-alerts-panel="config" style="${USERSCRIPTS_SECTION_STYLE}${isConfigTab ? "" : "display:none;"}">
                <div style="display:flex;justify-content:space-between;align-items:center;gap:8px;">
                    <div style="font-size:11px;font-weight:600;opacity:0.9;">Alert filters</div>
                    <span style="font-size:11px;opacity:0.8;">${enabledHazardCount}/${HAZARD_FILTER_OPTIONS.length} shown</span>
                </div>
                <div style="display:flex;gap:6px;align-items:stretch;">
                    <button type="button" data-live-alerts-hazard-bulk="all" style="${tabButtonStyle(false)}">Show all</button>
                    <button type="button" data-live-alerts-hazard-bulk="none" style="${tabButtonStyle(false)}">Hide all</button>
                </div>
                <div style="${USERSCRIPTS_DESCRIPTION_STYLE.replace("margin-bottom:8px;", "")}">Checked hazards and closures will appear on the map and in stats.</div>
                <div style="display:grid;gap:8px;max-height:220px;overflow:auto;">${hazardFilterHtml}</div>
            </div>
            <div data-live-alerts-panel="stats" style="${USERSCRIPTS_SECTION_STYLE}${isConfigTab ? "display:none;" : ""}">
                <div style="font-size:11px;font-weight:600;opacity:0.9;">Top alert subtypes</div>
                <div style="${USERSCRIPTS_STATUS_STYLE}">${statusText ? escapeHtml(statusText) : `${totalAlerts} total alerts match the current filters.`}</div>
                <div style="${USERSCRIPTS_LIST_STYLE}">${statsHtml}</div>
            </div>
        `;

    userscriptsPanelElement
      .querySelectorAll("[data-live-alerts-tab]")
      .forEach((/** @type {{ addEventListener: (arg0: string, arg1: () => void) => void; getAttribute: (arg0: string) => string; }} */ button) => {
        button.addEventListener("click", () => {
          userscriptsPanelActiveTab =
            button.getAttribute("data-live-alerts-tab") || "stats";
          updateUserscriptsPanelStats(alerts, statusText);
        });
      });

    userscriptsPanelElement
      .querySelectorAll("[data-live-alerts-hazard-filter]")
      .forEach((/** @type {{ addEventListener: (arg0: string, arg1: () => void) => void; getAttribute: (arg0: string) => any; checked: any; }} */ checkbox) => {
        checkbox.addEventListener("change", () => {
          const code = checkbox.getAttribute("data-live-alerts-hazard-filter");
          if (!code) {
            return;
          }

          const nextCodes = checkbox.checked
            ? [...visibleHazardCodes, code]
            : [...visibleHazardCodes].filter((value) => value !== code);
          setVisibleHazardCodes(nextCodes);
          refreshAlertVisibility(statusText);
        });
      });

    userscriptsPanelElement
      .querySelectorAll("[data-live-alerts-hazard-bulk]")
      .forEach((/** @type {{ addEventListener: (arg0: string, arg1: () => void) => void; getAttribute: (arg0: string) => any; }} */ button) => {
        button.addEventListener("click", () => {
          const action = button.getAttribute("data-live-alerts-hazard-bulk");
          setVisibleHazardCodes(
            action === "none" ? [] : DEFAULT_VISIBLE_HAZARD_CODES,
          );
          refreshAlertVisibility(statusText);
        });
      });

    userscriptsPanelElement
      .querySelectorAll("[data-live-alerts-hazard-group-action]")
      .forEach((/** @type {{ addEventListener: (arg0: string, arg1: () => void) => void; getAttribute: (arg0: string) => any; }} */ button) => {
        button.addEventListener("click", () => {
          const groupKey = button.getAttribute("data-live-alerts-hazard-group");
          const action = button.getAttribute(
            "data-live-alerts-hazard-group-action",
          );
          const group = HAZARD_FILTER_GROUPS.find(
            ({ key }) => key === groupKey,
          );
          if (!group) {
            return;
          }

          const nextCodes = new Set(visibleHazardCodes);
          group.options.forEach(({ code }) => {
            if (action === "none") {
              nextCodes.delete(code);
            } else {
              nextCodes.add(code);
            }
          });
          setVisibleHazardCodes([...nextCodes]);
          refreshAlertVisibility(statusText);
        });
      });

    userscriptsPanelElement
      .querySelectorAll("[data-live-alerts-filter-group-details]")
      .forEach((/** @type {{ addEventListener: (arg0: string, arg1: () => void) => void; getAttribute: (arg0: string) => any; open: boolean; }} */ detailsElement) => {
        detailsElement.addEventListener("toggle", () => {
          const groupKey = detailsElement.getAttribute(
            "data-live-alerts-filter-group-details",
          );
          if (!groupKey) {
            return;
          }

          const nextCollapsedGroupKeys = new Set(collapsedFilterGroupKeys);
          if (detailsElement.open) {
            nextCollapsedGroupKeys.delete(groupKey);
          } else {
            nextCollapsedGroupKeys.add(groupKey);
          }
          setCollapsedFilterGroupKeys([...nextCollapsedGroupKeys]);
        });
      });
  }

  /**
     * @param {{ Sidebar: { registerScriptTab: () => any; }; }} sdk
     */
  async function initUserscriptsPanel(sdk) {
    if (userscriptsPanelElement && userscriptsPanelElement.isConnected) {
      updateUserscriptsPanelStats(
        cachedAlerts,
        isAtVisibleZoom(sdk)
          ? ""
          : `Zoom in to ${MIN_VISIBLE_ZOOM}+ to load alerts.`,
      );
      return;
    }

    try {
      const registration = await sdk.Sidebar.registerScriptTab();
      const tabLabel = registration?.tabLabel;
      const tabPane = registration?.tabPane;
      if (tabLabel && "textContent" in tabLabel) {
        tabLabel.textContent = SCRIPT_TAB_LABEL;
      }

      const container =
        tabPane instanceof HTMLElement
          ? tabPane
          : extractSidebarContainerFromRegistrationResult(registration);
      const panelRoot = ensureUserscriptsContentRoot(container);
      if (!panelRoot) {
        return;
      }

      userscriptsPanelElement = panelRoot.querySelector(
        `#${SCRIPT_ID}-userscripts-panel`,
      );
      if (!(userscriptsPanelElement instanceof HTMLElement)) {
        userscriptsPanelElement = document.createElement("div");
        userscriptsPanelElement.id = `${SCRIPT_ID}-userscripts-panel`;
        panelRoot.appendChild(userscriptsPanelElement);
      }

      applyStyleText(userscriptsPanelElement, USERSCRIPTS_PANEL_STYLE);
      updateUserscriptsPanelStats(
        cachedAlerts,
        isAtVisibleZoom(sdk)
          ? ""
          : `Zoom in to ${MIN_VISIBLE_ZOOM}+ to load alerts.`,
      );
    } catch (error) {
      console.error(
        "[WME Live Alerts] Failed to initialize sidebar script tab",
        error,
      );
    }
  }

  /**
     * @param {{ Map: { getMapExtent: () => [any, any, any, any]; }; }} sdk
     */
  function getBoundsParams(sdk) {
    const [left, bottom, right, top] = sdk.Map.getMapExtent();
    return { top, left, bottom, right };
  }

  /**
     * @param {{ Map: { getZoomLevel: () => any; }; }} sdk
     */
  function getCurrentZoom(sdk) {
    const candidates = [
      Number(sdk?.Map?.getZoomLevel?.()),
      Number(window.W?.map?.getZoom?.()),
      Number(window.W?.map?.getOLMap?.()?.getView?.()?.getZoom?.()),
    ];
    for (const zoom of candidates) {
      if (Number.isFinite(zoom)) {
        return zoom;
      }
    }
    return null;
  }

  /**
     * @param {any} sdk
     */
  function isAtVisibleZoom(sdk) {
    return (getCurrentZoom(sdk) ?? -1) >= MIN_VISIBLE_ZOOM;
  }

  /**
     * @param {any} sdk
     */
  function shouldRequestAlerts(sdk) {
    return isAlertsLayerVisible && isAtVisibleZoom(sdk);
  }

  /**
     * @param {{ Map: { removeAllFeaturesFromLayer: (arg0: { layerName: string; }) => void; }; }} sdk
     */
  function clearAlertsFromLayer(sdk) {
    popupAlertsByFeatureId = new Map();
    closeAlertPopupState();
    sdk.Map.removeAllFeaturesFromLayer({
      layerName: ALERTS_LAYER_NAME,
    });
    updateUserscriptsPanelStats(
      [],
      `Zoom in to ${MIN_VISIBLE_ZOOM}+ to load alerts.`,
    );
  }

  /**
     * @param {{ left: number; right: number; bottom: number; top: number; }} innerBounds
     * @param {{ left: number; right: number; bottom: number; top: number; }} outerBounds
     */
  function isBoundsInside(innerBounds, outerBounds) {
    return (
      innerBounds.left >= outerBounds.left &&
      innerBounds.right <= outerBounds.right &&
      innerBounds.bottom >= outerBounds.bottom &&
      innerBounds.top <= outerBounds.top
    );
  }

  /**
     * @param {{ location: { x: any; lon: any; lng: any; y: any; lat: any; }; x: any; lon: any; lng: any; y: any; lat: any; }} alert
     */
  function getAlertLonLat(alert) {
    if (!alert || typeof alert !== "object") {
      return null;
    }

    if (alert.location && typeof alert.location === "object") {
      const lon = Number(
        alert.location.x ?? alert.location.lon ?? alert.location.lng,
      );
      const lat = Number(alert.location.y ?? alert.location.lat);
      if (Number.isFinite(lon) && Number.isFinite(lat)) {
        return { lon, lat };
      }
    }

    const lon = Number(alert.x ?? alert.lon ?? alert.lng);
    const lat = Number(alert.y ?? alert.lat);
    if (Number.isFinite(lon) && Number.isFinite(lat)) {
      return { lon, lat };
    }

    return null;
  }

  /**
     * @param {any} alert
     * @param {{ left: number; right: number; bottom: number; top: number; }} bounds
     */
  function isAlertInBounds(alert, bounds) {
    const lonLat = getAlertLonLat(alert);
    if (!lonLat) {
      return false;
    }

    return (
      lonLat.lon >= bounds.left &&
      lonLat.lon <= bounds.right &&
      lonLat.lat >= bounds.bottom &&
      lonLat.lat <= bounds.top
    );
  }

  /**
     * @param {{ alerts: any; }} data
     */
  function extractAlertsFromResponse(data) {
    return Array.isArray(data?.alerts) ? data.alerts : [];
  }

  /**
     * @param {{ subtype: string | number; type: string | number; }} alert
     */
  function getAlertSpriteUrl(alert) {
    if (!alert || typeof alert !== "object") {
      return null;
    }

    const hazardSpriteUrl = resolveSpriteUrl(
      ALERT_DEFINITIONS.HAZARD?.spriteUrl,
    );

    if (alert.subtype && ALERT_DEFINITIONS[alert.subtype]?.spriteUrl) {
      return resolveSpriteUrl(ALERT_DEFINITIONS[alert.subtype].spriteUrl);
    }

    return (
      resolveSpriteUrl(ALERT_DEFINITIONS[alert.type]?.spriteUrl) ||
      hazardSpriteUrl
    );
  }

  /**
     * @param {string} spriteUrl
     */
  function resolveSpriteUrl(spriteUrl) {
    if (typeof spriteUrl !== "string" || !spriteUrl) {
      return null;
    }

    if (
      spriteUrl.startsWith("http://") ||
      spriteUrl.startsWith("https://") ||
      spriteUrl.startsWith("data:")
    ) {
      return spriteUrl;
    }

    return `${ASSET_BASE_URL}${spriteUrl}`;
  }

  /**
     * @param {string} value
     */
  function humanizeAlertCode(value) {
    if (typeof value !== "string" || !value) {
      return null;
    }

    return value
      .toLowerCase()
      .split("_")
      .filter(Boolean)
      .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
      .join(" ");
  }

  /**
     * @param {string | number} value
     */
  function translateAlertCode(value) {
    return typeof value === "string" && value
      ? ALERT_DEFINITIONS[value]?.label || humanizeAlertCode(value) || value
      : null;
  }

  /**
     * @param {{ id: any; uuid: any; type: any; subtype: any; }} alert
     * @param {number} index
     */
  function buildAlertFeature(alert, index) {
    const lonLat = getAlertLonLat(alert);
    const spriteUrl = getAlertSpriteUrl(alert);
    if (!lonLat || !spriteUrl) {
      return null;
    }

    return {
      id: alert.id || alert.uuid || `live-alert-${index}`,
      type: "Feature",
      geometry: {
        type: "Point",
        coordinates: [lonLat.lon, lonLat.lat],
      },
      properties: {
        spriteUrl,
        alertType: alert.type || null,
        alertSubtype: alert.subtype || null,
        alertTypeText: translateAlertCode(alert.type),
        alertSubtypeText: translateAlertCode(alert.subtype),
        alertId: alert.id || null,
      },
    };
  }

  /**
     * @param {number} count
     */
  function buildClusterSpriteUrl(count) {
    const label = count > 99 ? "99+" : String(count);
    const fontSize = label.length >= 3 ? 18 : 20;
    const svg = `
            <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
                <circle cx="32" cy="32" r="24" fill="#f97316" stroke="#ffffff" stroke-width="3" />
                <text x="32" y="39" text-anchor="middle" fill="#ffffff" font-family="Arial, sans-serif" font-size="${fontSize}" font-weight="700">${label}</text>
            </svg>
        `;
    return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
  }

  /**
     * @param {{ alerts: any; lon: any; lat: any; lonSum?: number; latSum?: number; }} alertGroup
     * @param {number} index
     */
  function buildClusterFeature(alertGroup, index) {
    if (
      !alertGroup ||
      !Array.isArray(alertGroup.alerts) ||
      alertGroup.alerts.length < 2
    ) {
      return null;
    }

    const memberKey = alertGroup.alerts
      .map((/** @type {{ id: any; uuid: any; }} */ alert) => alert.id || alert.uuid || "")
      .filter(Boolean)
      .slice(0, 4)
      .join("-");

    return {
      id: `live-alert-cluster-${memberKey || index}-${alertGroup.alerts.length}-${Math.round(alertGroup.lon * 100000)}-${Math.round(alertGroup.lat * 100000)}`,
      type: "Feature",
      geometry: {
        type: "Point",
        coordinates: [alertGroup.lon, alertGroup.lat],
      },
      properties: {
        spriteUrl: buildClusterSpriteUrl(alertGroup.alerts.length),
        isCluster: true,
        clusterCount: alertGroup.alerts.length,
      },
    };
  }


  /**
     * @typedef {Object} AlertRenderGroup
     * @property {Array<any>} alerts
     * @property {number} lon
     * @property {number} lat
     * @property {number} lonSum
     * @property {number} latSum
     * @param {any} sdk
     * @param {any[]} alerts
     */
  function groupAlertsForRendering(sdk, alerts) {
    const bounds = getBoundsParams(sdk);
    const degPerPixelX =
      Math.abs(bounds.right - bounds.left) / Math.max(1, window.innerWidth);
    const degPerPixelY =
      Math.abs(bounds.top - bounds.bottom) / Math.max(1, window.innerHeight);
    const clusterDistancePx = 30;

    /** @type {Array<AlertRenderGroup>} */
    const groups = [];
    alerts.forEach((/** @type {any} */ alert) => {
      const lonLat = getAlertLonLat(alert);
      if (!lonLat) {
        return;
      }

      /** @type {AlertRenderGroup | null} */
      let nearestGroup = null;
      let nearestDistancePx = Number.POSITIVE_INFINITY;

      groups.forEach((group) => {
        const deltaX =
          degPerPixelX > 0
            ? (lonLat.lon - group.lon) / degPerPixelX
            : Number.POSITIVE_INFINITY;
        const deltaY =
          degPerPixelY > 0
            ? (lonLat.lat - group.lat) / degPerPixelY
            : Number.POSITIVE_INFINITY;
        const distancePx = Math.hypot(deltaX, deltaY);
        if (distancePx <= clusterDistancePx && distancePx < nearestDistancePx) {
          nearestGroup = group;
          nearestDistancePx = distancePx;
        }
      });

      if (!nearestGroup) {
        groups.push({
          alerts: [alert],
          lon: lonLat.lon,
          lat: lonLat.lat,
          lonSum: lonLat.lon,
          latSum: lonLat.lat,
        });
        return;
      }

      nearestGroup.alerts.push(alert);
      nearestGroup.lonSum += lonLat.lon;
      nearestGroup.latSum += lonLat.lat;
      nearestGroup.lon = nearestGroup.lonSum / nearestGroup.alerts.length;
      nearestGroup.lat = nearestGroup.latSum / nearestGroup.alerts.length;
    });

    return groups;
  }

  function ensurePopupElement() {
    if (popupElement) {
      return popupElement;
    }

    popupElement = document.createElement("div");
    popupElement.style.position = "fixed";
    popupElement.style.transform = "translate(-50%, -100%)";
    popupElement.style.zIndex = "10000";
    popupElement.style.minWidth = "180px";
    popupElement.style.maxWidth = "260px";
    popupElement.style.padding = "8px 10px";
    popupElement.style.borderRadius = "8px";
    popupElement.style.background = "#1f2937";
    popupElement.style.color = "#ffffff";
    popupElement.style.fontSize = "12px";
    popupElement.style.lineHeight = "1.35";
    popupElement.style.boxShadow = "0 6px 18px rgba(0, 0, 0, 0.35)";
    popupElement.style.pointerEvents = "auto";
    popupElement.style.display = "none";
    popupElement.style.whiteSpace = "normal";
    popupElement.style.border = "1px solid rgba(255, 255, 255, 0.12)";
    popupElement.style.overflowWrap = "anywhere";
    popupElement.style.wordBreak = "break-word";
    popupElement.style.hyphens = "auto";
    popupElement.style.overflowX = "hidden";
    popupElement.style.overflowY = "auto";
    popupElement.addEventListener("click", (event) => {
      event.stopPropagation();
    });
    document.body.appendChild(popupElement);
    return popupElement;
  }

  function hideAlertPopup() {
    const popup = ensurePopupElement();
    popup.style.display = "none";
    popup.innerHTML = "";
  }

  /**
     * @param {string} value
     */
  function escapeHtml(value) {
    return String(value)
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;")
      .replace(/'/g, "&#39;");
  }

  /**
     * @param {any} pubMillis
     */
  function formatAlertAge(pubMillis) {
    const publishedAt = Number(pubMillis);
    if (!Number.isFinite(publishedAt)) {
      return "Unknown";
    }

    const elapsedMs = Math.max(0, Date.now() - publishedAt);
    const totalMinutes = Math.floor(elapsedMs / 60000);

    if (totalMinutes < 60) {
      return `${totalMinutes}m`;
    }

    const totalHours = Math.floor(totalMinutes / 60);
    const minutes = totalMinutes % 60;
    if (totalHours < 24) {
      const paddedMinutes = String(minutes).padStart(2, "0");
      return `${totalHours}h ${paddedMinutes}m`;
    }

    const days = Math.floor(totalHours / 24);
    const hours = totalHours % 24;
    return `${days}d ${hours}h`;
  }

  /**
     * @param {number} segmentCount
     * @param {number | null} filledValue
     * @param {string} filledColor
     */
  function buildSegmentedBar(segmentCount, filledValue, filledColor) {
    return `<div style="display:grid;grid-template-columns:repeat(${segmentCount}, 1fr);gap:2px;">${Array.from(
      { length: segmentCount },
      (_, index) => {
        const isFilled = index < (filledValue ?? 0);
        const pillColor = isFilled ? filledColor : "rgba(255,255,255,0.16)";
        return `<span style="height:7px;border-radius:999px;background:${pillColor};"></span>`;
      },
    ).join("")}</div>`;
  }

  /**
     * @param {any} alerts
     * @param {number} pageIndex
     */
  function getAlertPopupHtml(alerts, pageIndex) {
    const popupAlerts = Array.isArray(alerts) ? alerts : [];
    const totalPages = popupAlerts.length;
    const safePageIndex = totalPages
      ? Math.max(0, Math.min(totalPages - 1, pageIndex || 0))
      : 0;
    const alert = popupAlerts[safePageIndex];
    if (!alert) {
      return "";
    }

    const typeLabel = translateAlertCode(alert.type) || "Unknown alert type";
    const subtypeLabel = translateAlertCode(alert.subtype);
    const typeText =
      subtypeLabel && subtypeLabel !== typeLabel
        ? `${typeLabel} • ${subtypeLabel}`
        : typeLabel;

    const locationText =
      alert.street || alert.nearBy || alert.city || "Unknown location";
    const thumbsUpCount = Number.isFinite(Number(alert.nThumbsUp))
      ? Number(alert.nThumbsUp)
      : 0;
    const additionalInfoValue =
      typeof (alert.additionalInfo || alert.provider || "") === "string"
        ? (alert.additionalInfo || alert.provider || "").trim()
        : "";
    const additionalInfoText = additionalInfoValue
      ? escapeHtml(additionalInfoValue).replace(/\n/g, "<br>")
      : "—";
    const descriptionValue =
      typeof (alert.reportDescription || "") === "string"
        ? (alert.reportDescription || "").trim()
        : "";
    const descriptionText = descriptionValue
      ? escapeHtml(descriptionValue).replace(/\n/g, "<br>")
      : "—";
    const alertAgeText = formatAlertAge(alert.pubMillis);
    const reliabilityNumeric = Number(alert.reliability);
    const reliabilityValue = Number.isFinite(reliabilityNumeric)
      ? Math.max(0, Math.min(10, Math.round(reliabilityNumeric)))
      : null;
    const reliabilityLabel =
      reliabilityValue === null ? "N/A" : `${reliabilityValue}/10`;
    const reliabilityColor =
      reliabilityValue === null
        ? "#6b7280"
        : `hsl(${Math.round((reliabilityValue / 10) * 120)}, 85%, 45%)`;
    const confidenceNumeric = Number(alert.confidence);
    const confidenceValue = Number.isFinite(confidenceNumeric)
      ? Math.max(0, Math.min(5, Math.round(confidenceNumeric)))
      : null;
    const confidenceLabel =
      confidenceValue === null ? "N/A" : `${confidenceValue}/5`;
    const confidenceColor =
      confidenceValue === null
        ? "#6b7280"
        : `hsl(${Math.round((confidenceValue / 5) * 120)}, 85%, 45%)`;
    const reliabilityBarHtml = buildSegmentedBar(
      10,
      reliabilityValue,
      reliabilityColor,
    );
    const confidenceBarHtml = buildSegmentedBar(
      5,
      confidenceValue,
      confidenceColor,
    );
    const detailsHtml = [
      ["Location", escapeHtml(locationText)],
      ["Thumbs up", thumbsUpCount],
      ["Age", escapeHtml(alertAgeText)],
      ["Additional info", additionalInfoText],
    ]
      .map(
        ([label, value]) =>
          `<div style="margin-bottom:4px;"><strong>${label}:</strong> ${value}</div>`,
      )
      .join("");
    const paginationHtml =
      totalPages > 1
        ? `
                <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:6px;">
                    <button type="button" data-popup-prev="true" style="border:1px solid rgba(255,255,255,0.25);background:rgba(255,255,255,0.08);color:#ffffff;border-radius:4px;padding:1px 6px;cursor:pointer;">‹ Prev</button>
                    <span style="font-size:11px;opacity:0.9;">${safePageIndex + 1} / ${totalPages}</span>
                    <button type="button" data-popup-next="true" style="border:1px solid rgba(255,255,255,0.25);background:rgba(255,255,255,0.08);color:#ffffff;border-radius:4px;padding:1px 6px;cursor:pointer;">Next ›</button>
                </div>
            `
        : "";

    return `
            <div style="display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:6px;">
                <div style="font-weight:600;font-size:12px;">${escapeHtml(typeText)}</div>
                <button type="button" data-popup-close="true" title="Close" style="border:0;background:transparent;color:#ffffff;cursor:pointer;font-size:16px;line-height:1;padding:0 2px;">×</button>
            </div>
            ${paginationHtml}
            ${detailsHtml}
            <div style="margin-bottom:6px;"><strong>Description:</strong> ${descriptionText}</div>
            <div style="display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:4px;">
                <strong>Reliability</strong>
                <span>${reliabilityLabel}</span>
            </div>
            <div style="margin-bottom:6px;">
                ${reliabilityBarHtml}
            </div>
            <div style="display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:4px;">
                <strong>Confidence</strong>
                <span>${confidenceLabel}</span>
            </div>
            <div style="margin-bottom:6px;">
                ${confidenceBarHtml}
            </div>
            <div style="font-size:10px;line-height:1.2;color:rgba(255,255,255,0.58);text-align:right;">
                Sourced from Live Map
            </div>
        `;
  }

  /**
     * @param {{ Map: { getFeatureDomElement: (arg0: { layerName: string; featureId: any; }) => any; }; }} sdk
     * @param {any} featureId
     */
  function showAlertPopupForFeature(sdk, featureId) {
    const featureKey = String(featureId);
    const popupAlerts = popupAlertsByFeatureId.get(featureKey);
    if (!popupAlerts || !popupAlerts.length) {
      hideAlertPopup();
      return;
    }

    const totalPages = popupAlerts.length;
    const pageIndex = Math.max(
      0,
      Math.min(totalPages - 1, popupPageByFeatureId.get(featureKey) || 0),
    );
    popupPageByFeatureId.set(featureKey, pageIndex);

    const featureElement = sdk.Map.getFeatureDomElement({
      layerName: ALERTS_LAYER_NAME,
      featureId,
    });
    if (!featureElement) {
      hideAlertPopup();
      return;
    }

    const rect = featureElement.getBoundingClientRect();
    const popup = ensurePopupElement();
    popup.innerHTML = getAlertPopupHtml(popupAlerts, pageIndex);

    const viewportPadding = 12;
    const maxPopupWidth = Math.max(
      220,
      window.innerWidth - viewportPadding * 2,
    );
    const maxPopupHeight = Math.max(
      160,
      window.innerHeight - viewportPadding * 2,
    );
    popup.style.maxWidth = `${maxPopupWidth}px`;
    popup.style.maxHeight = `${maxPopupHeight}px`;
    popup.style.visibility = "hidden";
    popup.style.display = "block";

    const popupWidth = popup.offsetWidth;
    const popupHeight = popup.offsetHeight;
    let left = rect.left + rect.width / 2;
    let top = rect.top - 8;

    if (left - popupWidth / 2 < viewportPadding) {
      left = viewportPadding + popupWidth / 2;
    }
    if (left + popupWidth / 2 > window.innerWidth - viewportPadding) {
      left = window.innerWidth - viewportPadding - popupWidth / 2;
    }

    const canRenderAbove = top - popupHeight >= viewportPadding;
    if (canRenderAbove) {
      popup.style.transform = "translate(-50%, -100%)";
      top = Math.max(viewportPadding + popupHeight, top);
    } else {
      popup.style.transform = "translate(-50%, 0)";
      top = Math.min(
        window.innerHeight - viewportPadding - popupHeight,
        Math.max(viewportPadding, rect.bottom + 8),
      );
    }

    const closeButton = popup.querySelector('[data-popup-close="true"]');
    if (closeButton) {
      closeButton.addEventListener("click", (/** @type {{ stopPropagation: () => void; }} */ event) => {
        event.stopPropagation();
        closeAlertPopupState();
      });
    }
    const prevButton = popup.querySelector('[data-popup-prev="true"]');
    if (prevButton) {
      prevButton.addEventListener("click", (/** @type {{ stopPropagation: () => void; }} */ event) => {
        event.stopPropagation();
        const currentPage = popupPageByFeatureId.get(featureKey) || 0;
        popupPageByFeatureId.set(
          featureKey,
          currentPage <= 0 ? totalPages - 1 : currentPage - 1,
        );
        showAlertPopupForFeature(sdk, featureId);
      });
    }
    const nextButton = popup.querySelector('[data-popup-next="true"]');
    if (nextButton) {
      nextButton.addEventListener("click", (/** @type {{ stopPropagation: () => void; }} */ event) => {
        event.stopPropagation();
        const currentPage = popupPageByFeatureId.get(featureKey) || 0;
        popupPageByFeatureId.set(
          featureKey,
          currentPage >= totalPages - 1 ? 0 : currentPage + 1,
        );
        showAlertPopupForFeature(sdk, featureId);
      });
    }
    popup.style.left = `${left}px`;
    popup.style.top = `${top}px`;
    popup.style.visibility = "visible";
  }

  function closeAlertPopupState() {
    hoveredFeatureId = null;
    pinnedFeatureId = null;
    popupPageByFeatureId.clear();
    hideAlertPopup();
  }

  /**
     * @param {{ Events: { trackLayerEvents: (arg0: { layerName: string; }) => void; }; }} sdk
     */
  function initAlertPopupHandlers(sdk) {
    sdk.Events.trackLayerEvents({ layerName: ALERTS_LAYER_NAME });

    onEvent(
      sdk,
      "wme-layer-feature-mouse-enter",
      ({ layerName, featureId }) => {
        if (layerName !== ALERTS_LAYER_NAME || pinnedFeatureId) {
          return;
        }

        hoveredFeatureId = featureId;
        showAlertPopupForFeature(sdk, featureId);
      },
    );

    onEvent(
      sdk,
      "wme-layer-feature-mouse-leave",
      ({ layerName, featureId }) => {
        if (layerName !== ALERTS_LAYER_NAME || pinnedFeatureId) {
          return;
        }

        if (hoveredFeatureId === featureId) {
          hoveredFeatureId = null;
          hideAlertPopup();
        }
      },
    );

    onEvent(sdk, "wme-layer-feature-clicked", ({ layerName, featureId }) => {
      if (layerName !== ALERTS_LAYER_NAME) {
        return;
      }

      pinnedFeatureId = featureId;
      hoveredFeatureId = featureId;
      ignoreNextMapClickClose = true;
      setTimeout(() => {
        ignoreNextMapClickClose = false;
      }, 100);
      showAlertPopupForFeature(sdk, featureId);
    });

    onEvent(sdk, "wme-map-mouse-click", () => {
      if (ignoreNextMapClickClose) {
        return;
      }

      closeAlertPopupState();
    });

    document.addEventListener("mousedown", (event) => {
      const popup = ensurePopupElement();
      if (popup.style.display !== "none" && !popup.contains(event.target)) {
        closeAlertPopupState();
      }
    });
  }

  /**
     * @param {{ Map: { removeAllFeaturesFromLayer: (arg0: { layerName: string; }) => void; addFeaturesToLayer: (arg0: { layerName: string; features: ({ id: any; type: string; geometry: { type: string; coordinates: number[]; }; properties: { spriteUrl: string; alertType: any; alertSubtype: any; alertTypeText: string | null; alertSubtypeText: string | null; alertId: any; }; } | { id: string; type: string; geometry: { type: string; coordinates: any[]; }; properties: { spriteUrl: string; isCluster: boolean; clusterCount: any; }; } | null)[]; }) => void; }; }} sdk
     * @param {any[]} alerts
     */
  function renderAlertsOnLayer(sdk, alerts) {
    if (!isAtVisibleZoom(sdk)) {
      clearAlertsFromLayer(sdk);
      return;
    }

    const visibleAlerts = getFilteredAlerts(alerts);
    popupAlertsByFeatureId = new Map();
    const alertGroups = groupAlertsForRendering(sdk, visibleAlerts);
    const features = alertGroups
      .map((alertGroup, index) => {
        if (alertGroup.alerts.length > 1) {
          const clusterFeature = buildClusterFeature(alertGroup, index);
          if (clusterFeature) {
            const sortedClusterAlerts = [...alertGroup.alerts].sort(
              (leftAlert, rightAlert) => {
                const leftPubMillis = Number(leftAlert?.pubMillis) || 0;
                const rightPubMillis = Number(rightAlert?.pubMillis) || 0;
                return rightPubMillis - leftPubMillis;
              },
            );
            popupAlertsByFeatureId.set(
              String(clusterFeature.id),
              sortedClusterAlerts,
            );
          }
          return clusterFeature;
        }

        const singleAlert = alertGroup.alerts[0];
        const feature = buildAlertFeature(singleAlert, index);
        if (feature) {
          popupAlertsByFeatureId.set(String(feature.id), [singleAlert]);
        }
        return feature;
      })
      .filter(Boolean);

    sdk.Map.removeAllFeaturesFromLayer({
      layerName: ALERTS_LAYER_NAME,
    });

    if (features.length) {
      sdk.Map.addFeaturesToLayer({
        layerName: ALERTS_LAYER_NAME,
        features,
      });
    }

    if (
      pinnedFeatureId &&
      popupAlertsByFeatureId.has(String(pinnedFeatureId))
    ) {
      showAlertPopupForFeature(sdk, pinnedFeatureId);
    } else if (
      hoveredFeatureId &&
      popupAlertsByFeatureId.has(String(hoveredFeatureId)) &&
      !pinnedFeatureId
    ) {
      showAlertPopupForFeature(sdk, hoveredFeatureId);
    } else {
      closeAlertPopupState();
    }
  }

  /**
     * @param {{ top: any; left: any; bottom: any; right: any; }} bounds
     */
  function shouldSkipFetchForBounds(bounds) {
    if (!lastFetchedBounds || !isBoundsInside(bounds, lastFetchedBounds)) {
      return false;
    }
    return cachedAlerts.some((alert) => isAlertInBounds(alert, bounds));
  }

  /**
     * @param {any} sdk
     * @param {{ top: any; left: any; bottom: any; right: any; }} bounds
     */
  async function fetchAlerts(sdk, bounds) {
    if (!shouldRequestAlerts(sdk)) {
      clearAlertsFromLayer(sdk);
      return;
    }

    const queryParams = new URLSearchParams({
      top: String(bounds.top),
      bottom: String(bounds.bottom),
      left: String(bounds.left),
      right: String(bounds.right),
      types: "alerts", // Only fetch alerts (exclude jams, cameras, etc.)
      env: DEFAULT_ENV,
    });

    const url = `${LIVEMAP_API_URL}?${queryParams.toString()}`;

    try {
      if (activeRequestController) {
        activeRequestController.abort();
      }

      activeRequestController = new AbortController();
      const response = await fetch(url, {
        method: "GET",
        credentials: "include",
        signal: activeRequestController.signal,
      });
      const contentType = response.headers.get("content-type") || "";
      const data = contentType.includes("application/json")
        ? await response.json()
        : await response.text();

      if (response.ok && contentType.includes("application/json")) {
        cachedAlerts = extractAlertsFromResponse(data);
        lastFetchedBounds = bounds;
        renderAlertsOnLayer(sdk, cachedAlerts);
        updateUserscriptsPanelStats(cachedAlerts);
      } else {
        console.warn("[WME Live Alerts] Unexpected Live Map response", {
          status: response.status,
          ok: response.ok,
          contentType,
          url,
        });
      }
    } catch (error) {
      if (error && error.name === "AbortError") {
        return;
      }
      console.error("[WME Live Alerts] Failed to fetch Live Map alerts", error);
    } finally {
      activeRequestController = null;
    }
  }

  /**
     * @param {{ Map: { addLayer: (arg0: { layerName: string; styleContext: { getSpriteUrl: ({ feature }: { feature: any; }) => any; }; styleRules: { style: { externalGraphic: string; graphicWidth: number; graphicHeight: number; graphicXOffset: number; graphicYOffset: number; graphicOpacity: number; }; }[]; }) => void; setLayerVisibility: (arg0: { layerName: string; visibility: any; }) => void; }; LayerSwitcher: { addLayerCheckbox: (arg0: { name: string; isChecked: boolean; }) => void; }; }} sdk
     */
  function initAlertsLayer(sdk) {
    try {
      const initialLayerVisible = getStoredLayerVisibility();

      sdk.Map.addLayer({
        layerName: ALERTS_LAYER_NAME,
        styleContext: {
          getSpriteUrl: ({ feature }) =>
            feature?.properties?.spriteUrl ||
            resolveSpriteUrl(ALERT_DEFINITIONS.HAZARD?.spriteUrl),
        },
        styleRules: [
          {
            style: {
              externalGraphic: "${getSpriteUrl}",
              graphicWidth: 41.5,
              graphicHeight: 46,
              graphicXOffset: -20.75,
              graphicYOffset: -46,
              graphicOpacity: 1,
            },
          },
        ],
      });

      sdk.LayerSwitcher.addLayerCheckbox({
        name: ALERTS_LAYER_CHECKBOX_NAME,
        isChecked: initialLayerVisible,
      });

      sdk.Map.setLayerVisibility({
        layerName: ALERTS_LAYER_NAME,
        visibility: initialLayerVisible,
      });
      isAlertsLayerVisible = initialLayerVisible;

      onEvent(sdk, "wme-layer-checkbox-toggled", ({ name, checked }) => {
        if (name !== ALERTS_LAYER_CHECKBOX_NAME) {
          return;
        }

        isAlertsLayerVisible = checked;
        setStoredLayerVisibility(checked);

        sdk.Map.setLayerVisibility({
          layerName: ALERTS_LAYER_NAME,
          visibility: checked,
        });

        if (!checked) {
          if (activeRequestController) {
            activeRequestController.abort();
          }
          closeAlertPopupState();
        }
      });

      initAlertPopupHandlers(sdk);
    } catch (error) {
      console.error(
        "[WME Live Alerts] Failed to initialize layer and checkbox",
        {
          layerName: ALERTS_LAYER_NAME,
          checkboxName: ALERTS_LAYER_CHECKBOX_NAME,
          error,
        },
      );
    }
  }

  function initScript() {
    const sdk = window.getWmeSdk({
      scriptId: SCRIPT_ID,
      scriptName: SCRIPT_NAME,
    });
    sdkInstance = sdk;

    const debouncedFetchAlerts = debounce(() => {
      const bounds = getBoundsParams(sdk);
      if (shouldSkipFetchForBounds(bounds)) {
        return;
      }

      fetchAlerts(sdk, bounds);
    }, MAP_MOVE_FETCH_DEBOUNCE_MS);

    sdk.Events.once({ eventName: "wme-ready" }).then(() => {
      initAlertsLayer(sdk);
      initUserscriptsPanel(sdk);

      if (shouldRequestAlerts(sdk)) {
        fetchAlerts(sdk, getBoundsParams(sdk));
      } else {
        clearAlertsFromLayer(sdk);
      }

      onEvent(sdk, "wme-map-move", () => {
        if (!shouldRequestAlerts(sdk)) {
          if (activeRequestController) activeRequestController.abort();
          clearAlertsFromLayer(sdk);
          return;
        }

        debouncedFetchAlerts();
      });
    });
  }

  function waitForSdkAndInit() {
    if (
      window.SDK_INITIALIZED &&
      typeof window.SDK_INITIALIZED.then === "function"
    ) {
      window.SDK_INITIALIZED.then(initScript);
    } else {
      console.error("[WME Live Alerts] SDK_INITIALIZED is unavailable");
    }
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", waitForSdkAndInit, {
      once: true,
    });
  } else {
    waitForSdkAndInit();
  }
})();