WME Polygon Issue Filter

Filter Place Update Requests (PURs) inside custom WKT polygons. No external dependencies. Shows matching PUR venues in the sidebar with toggleable polygon layers.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name         WME Polygon Issue Filter
// @namespace    https://greatest.deepsurf.us/en/users/1393710-majorjcms
// @version      2026.05.31.19
// @description  Filter Place Update Requests (PURs) inside custom WKT polygons. No external dependencies. Shows matching PUR venues in the sidebar with toggleable polygon layers.
// @author       majorjc_MS
// @match        https://*.waze.com/*editor*
// @exclude      https://*.waze.com/user/editor*
// @grant        none
// @supportURL   https://github.com/MajorjcMS/WME_PolygonIssueFilter/issues
// @license      MIT
// ==/UserScript==

/* global W */

// MIT License - see LICENSE file for full text
// https://github.com/MajorjcMS/WME_PolygonIssueFilter/blob/main/LICENSE

(async function () {
  'use strict';

  const SCRIPT_NAME = 'WME Polygon Issue Filter';
  const SCRIPT_VERSION = GM_info.script.version;
  const STORAGE_KEY_PREFIX = 'wme_polygon_issue_filter';

  await window.SDK_INITIALIZED;

  const wmeSdk = getWmeSdk({
    scriptId: 'wme-polygon-issue-filter',
    scriptName: SCRIPT_NAME
  });

  if (!wmeSdk.State.isInitialized()) {
    await wmeSdk.Events.once({ eventName: 'wme-initialized' });
  }

  console.log(`[${SCRIPT_NAME}] SDK initialized`);

  // Wait for WMETB (WME Toolbox) to finish loading, if it is present or loads later
  console.log(`[${SCRIPT_NAME}] Waiting for WMETB (if present)...`);
  await new Promise(resolve => {
    const interval = setInterval(() => {
      if (window.WMETB && window.WMETB.loaded) {
        clearInterval(interval);
        console.log(`[${SCRIPT_NAME}] WMETB finished loading`);
        resolve();
      }
    }, 100);

    // Safety timeout (15s) so we don't block forever if WMETB is not installed
    setTimeout(() => {
      clearInterval(interval);
      resolve();
    }, 15000);
  });

  // Diagnostic: Log available Map methods
  try {
    const mapMethods = Object.getOwnPropertyNames(wmeSdk.Map)
      .filter(k => typeof wmeSdk.Map[k] === 'function')
      .sort();
    console.log(`[${SCRIPT_NAME}] Available wmeSdk.Map methods:`, mapMethods);

    if (mapMethods.length === 0) {
      console.warn(`[${SCRIPT_NAME}] wmeSdk.Map currently reports no methods (this is common in some WME builds). Using SDK layer pattern anyway (following WME Geometries script).`);
    }
  } catch (e) {
    console.warn(`[${SCRIPT_NAME}] Could not list Map methods`, e);
  }

  // Listen for layer checkbox toggles in the Layers panel.
  // The checkbox shows the custom name, so we map it back to the internal layerName.
  wmeSdk.Events.on({
    eventName: "wme-layer-checkbox-toggled",
    eventHandler(payload) {
      for (const poly of polygons.values()) {
        if (poly.name === payload.name) {
          try {
            wmeSdk.Map.setLayerVisibility({
              layerName: poly.layerName,
              visibility: payload.checked,
            });
          } catch (e) {}
          return;
        }
      }
    },
  });

  // ==================== STATE ====================
  let polygons = new Map();   // id → { id, name, wkt, layerName }
  let userName = null;

  // Safe DOM clearing helper (avoids innerHTML='' Trusted Types violations)
  function clearElement(el) {
    if (!el) return;
    while (el.firstChild) el.removeChild(el.firstChild);
  }

  // ==================== STORAGE ====================
  function getStorageKey(suffix) {
    const user = userName || 'default';
    return `${STORAGE_KEY_PREFIX}_${user}_${suffix}`;
  }

  function loadPolygons() {
    const saved = localStorage.getItem(getStorageKey('polygons'));
    polygons.clear();

    if (saved) {
      try {
        const arr = JSON.parse(saved);
        for (const p of arr) {
          if (p && p.id && p.name && p.wkt) {
            const layerName = `wme-pif-${p.id}`;
            polygons.set(p.id, {
              id: p.id,
              name: p.name,
              wkt: p.wkt,
              layerName
            });
          }
        }
        console.log(`[${SCRIPT_NAME}] Loaded ${polygons.size} saved polygons`);
      } catch (e) {
        console.warn(`[${SCRIPT_NAME}] Failed to load saved polygons`, e);
      }
    }
  }

  function savePolygons() {
    const arr = Array.from(polygons.values()).map(p => ({
      id: p.id,
      name: p.name,
      wkt: p.wkt
    }));
    localStorage.setItem(getStorageKey('polygons'), JSON.stringify(arr));
  }

  function clearAllPolygons() {
    for (const p of polygons.values()) {
      try {
        wmeSdk.Map.removeLayer({ layerName: p.layerName });
        wmeSdk.LayerSwitcher.removeLayerCheckbox({ name: p.layerName });
      } catch (_) {}
    }
    polygons.clear();
    localStorage.removeItem(getStorageKey('polygons'));
  }

  // ==================== SCRIPT ENABLED STATE ====================
  function isScriptEnabled() {
    const val = localStorage.getItem(getStorageKey('enabled'));
    return val === null ? true : val === 'true'; // default: ON
  }

  function setScriptEnabled(enabled) {
    localStorage.setItem(getStorageKey('enabled'), String(enabled));
  }

  // ==================== WKT + GEOMETRY ====================

  /**
   * Very basic WKT parser for simple POLYGON only.
   * Returns a GeoJSON Polygon geometry or null.
   */
  function parseSimpleWktPolygon(wkt) {
    if (!wkt) return null;

    // Match POLYGON(( ... )) — case insensitive, allows extra whitespace
    const match = wkt.match(/^\s*POLYGON\s*\(\s*\(\s*(.+?)\s*\)\s*\)\s*$/i);
    if (!match) return null;

    const ringStr = match[1];
    const coordinates = [];

    const pairs = ringStr.split(/\s*,\s*/);
    for (const pair of pairs) {
      const nums = pair.trim().split(/\s+/);
      if (nums.length !== 2) return null;

      const x = parseFloat(nums[0]);
      const y = parseFloat(nums[1]);
      if (isNaN(x) || isNaN(y)) return null;

      coordinates.push([x, y]);
    }

    if (coordinates.length < 3) return null;

    return {
      type: 'Polygon',
      coordinates: [coordinates]
    };
  }

  /**
   * Simple point-in-polygon test using ray casting.
   * point = [lon, lat]
   * polygon = GeoJSON Polygon geometry (as returned by parseSimpleWktPolygon)
   */
  function pointInPolygon(point, polygon) {
    if (!polygon || polygon.type !== 'Polygon') return false;

    const [x, y] = point;
    const ring = polygon.coordinates[0];
    if (!ring || ring.length < 3) return false;

    let inside = false;
    for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
      const [xi, yi] = ring[i];
      const [xj, yj] = ring[j];

      const intersect = ((yi > y) !== (yj > y)) &&
        (x < (xj - xi) * (y - yi) / (yj - yi + 0.0000001) + xi);
      if (intersect) inside = !inside;
    }
    return inside;
  }

  /**
   * Get a representative [lon, lat] point for a venue.
   */
  function getVenuePoint(venue) {
    if (!venue.geometry) return null;

    const geom = venue.geometry;
    if (geom.type === 'Point') {
      return geom.coordinates;
    }
    if (geom.type === 'Polygon' || geom.type === 'MultiPolygon') {
      // Use first coordinate of the outer ring as approximation
      const outer = geom.type === 'Polygon' ? geom.coordinates[0] : geom.coordinates[0][0];
      if (outer && outer.length > 0) return outer[0];
    }
    return null;
  }

  /**
   * Collects multiple candidate IDs that might work with showVenueUpdateRequestDialog.
   * WME SDK data model IDs are very inconsistent across builds and even within the same session.
   */
  function getCandidateIdsForPurDialog(venue, purs = []) {
    const candidates = new Set();

    const add = (val) => {
      if (val != null && val !== '') {
        candidates.add(String(val));
      }
    };

    // Venue-level candidates
    if (venue) {
      add(venue.id);
      add(venue.venueId);
      add(venue.attributes?.id);
    }

    // PUR-level candidates (often the most reliable for this dialog)
    for (const pur of purs) {
      if (!pur) continue;
      add(pur.id);                    // PUR's own ID is sometimes accepted
      add(pur.venueId);
      add(pur.venue?.id);
      add(pur.venue?.attributes?.id);
      add(pur.placeId);
      add(pur.attributes?.id);
      add(pur.attributes?.venueId);
      add(pur.attributes?.placeId);
    }

    return Array.from(candidates);
  }

  // ==================== MAP LAYER ====================




  function removePolygonLayer() {
    // Legacy cleanup only (for very old test layers from previous versions of this script).
    if (window._pifLegacyLayer && window.W && W.map) {
      try { W.map.removeLayer(window._pifLegacyLayer); } catch (_) {}
      window._pifLegacyLayer = null;
    }
  }

  // ==================== SNACKBAR (replaces WazeWrap.Alerts) ====================
  function showSnackbar(message, type = 'info') {
    const colors = {
      success: '#2e7d32',
      error: '#c62828',
      warning: '#f57c00',
      info: '#1565c0'
    };

    // Create elements natively to avoid Trusted Types violations from jQuery
    const snackbar = document.createElement('wz-snackbar');
    snackbar.setAttribute('align', 'center');
    snackbar.style.setProperty('--wz-snackbar-position', 'absolute');

    if (type === 'error') {
      snackbar.setAttribute('close-button', 'true');
    }

    const text = document.createElement('span');
    text.textContent = message;
    snackbar.appendChild(text);

    const container = document.getElementById('map-message-container');
    if (container) {
      container.appendChild(snackbar);
    } else {
      document.body.appendChild(snackbar);
    }

    // Show it (Waze custom element method)
    snackbar.showSnackbar?.();

    // Auto-hide after a few seconds unless it's an error
    if (type !== 'error') {
      setTimeout(() => {
        try { snackbar.hideSnackbar?.(); } catch (_) {}
        try { snackbar.remove(); } catch (_) {}
      }, 3200);
    }
  }

  // ==================== SIDEBAR TAB ====================
  async function createSidebarTab() {
    // Use the exact pattern from WME Junction Angle Info (JAI) for a proper
    // clickable tab pill in the Scripts panel with power icon + short label.
    // registerScriptTab() returns { tabLabel, tabPane }. We populate tabLabel
    // directly (safe DOM, no innerHTML) so it renders exactly like JAI's "⏻ JAI".
    const { tabLabel, tabPane } = await wmeSdk.Sidebar.registerScriptTab();

    // Build tab button content safely (power icon + "PI" short label)
    const powerSpan = document.createElement('span');
    powerSpan.id = 'wme-pif-power-btn';
    powerSpan.className = 'fa fa-power-off';
    powerSpan.style.cssText = 'margin-right:5px;cursor:pointer;color:#4CAF50;font-size:13px;';
    powerSpan.title = 'Toggle Polygon Issue Filter on/off';

    const textSpan = document.createElement('span');
    textSpan.textContent = 'PI';
    textSpan.title = 'WME Polygon Issue Filter';

    tabLabel.appendChild(powerSpan);
    tabLabel.appendChild(textSpan);

    tabPane.id = 'sidepanel-pif';

    // Main container
    const container = document.createElement('div');
    container.style.cssText = 'padding: 8px 12px; display: flex; flex-direction: column; gap: 10px;';

    // Dynamic content area (no duplicate header/power row — power lives on the tab pill like JAI)
    const dynamicContent = document.createElement('div');
    container.appendChild(dynamicContent);

    function renderDisabledUI() {
      dynamicContent.innerHTML = '';
      const msg = document.createElement('div');
      msg.style.cssText = 'padding: 16px 4px; color: #888; font-size: 13px; text-align: center; line-height: 1.4;';
      msg.textContent = 'Script is disabled. Click the power icon on the PI tab to enable polygon filtering and PUR detection.';
      dynamicContent.appendChild(msg);
    }

    function renderFullUI() {
      dynamicContent.innerHTML = '';

      // === POLYGON MANAGEMENT SECTION ===
      const polySection = document.createElement('div');

      const title = document.createElement('wz-label');
      title.textContent = 'My Filter Polygons';
      polySection.appendChild(title);

      const polygonList = document.createElement('ul');
      polygonList.id = 'wme-pif-polygon-list';
      polygonList.style.cssText = 'list-style: none; padding: 0; margin: 8px 0; font-size: 13px;';
      polySection.appendChild(polygonList);

      const addForm = document.createElement('div');
      addForm.style.cssText = 'border-top: 1px solid #ccc; padding-top: 8px; margin-top: 8px;';

      const nameLabel = document.createElement('wz-label');
      nameLabel.textContent = 'Name';
      nameLabel.style.cssText = 'display: block; margin-bottom: 4px;';

      const nameInput = document.createElement('input');
      nameInput.type = 'text';
      nameInput.placeholder = 'Name for Polygon';
      nameInput.style.cssText = 'width: 100%; margin-bottom: 6px;';

      const wktLabel = document.createElement('wz-label');
      wktLabel.textContent = 'WKT Polygon';
      wktLabel.style.cssText = 'display: block; margin-bottom: 4px;';

      const wktInput = document.createElement('textarea');
      wktInput.rows = 3;
      wktInput.placeholder = 'POLYGON((lon lat, ...))';
      wktInput.style.cssText = 'width: 100%; font-family: monospace; font-size: 12px; resize: vertical; margin-bottom: 6px;';

      const addBtn = createWzButton('Add Polygon', 'primary', () => {
        const name = nameInput.value.trim();
        const wkt = wktInput.value.trim();
        if (!name || !wkt) {
          showSnackbar('Please provide both a name and WKT', 'warning');
          return;
        }
        addNewPolygon(name, wkt);
        nameInput.value = '';
        wktInput.value = '';
      });

      addForm.appendChild(nameLabel);
      addForm.appendChild(nameInput);
      addForm.appendChild(wktLabel);
      addForm.appendChild(wktInput);
      addForm.appendChild(addBtn);
      polySection.appendChild(addForm);

      // === PUR LIST SECTION ===
      const listSection = document.createElement('div');

      const headerRow = document.createElement('div');
      headerRow.style.cssText = 'display: flex; justify-content: space-between; align-items: center;';

      const purLabel = document.createElement('wz-label');
      purLabel.textContent = 'PURs in your Polygons';

      const countSpan = document.createElement('span');
      countSpan.id = 'wme-pif-count';
      countSpan.style.cssText = 'font-size: 12px; color: #666;';

      headerRow.appendChild(purLabel);
      headerRow.appendChild(countSpan);
      listSection.appendChild(headerRow);

      const listContainer = document.createElement('div');
      listContainer.id = 'wme-pif-list';
      listContainer.style.cssText = `
        border: 1px solid #ddd;
        border-radius: 4px;
        max-height: 420px;
        overflow-y: auto;
        background: #fff;
        font-size: 13px;
      `;

      const refreshBtn = createWzButton('Refresh', 'text', () => {
        refreshBtn.disabled = true;
        const originalText = refreshBtn.textContent;
        refreshBtn.textContent = 'Refreshing...';
        refreshIssueList();
        setTimeout(() => {
          refreshBtn.disabled = false;
          refreshBtn.textContent = originalText;
        }, 400);
      });
      refreshBtn.style.marginTop = '6px';

      listSection.appendChild(listContainer);
      listSection.appendChild(refreshBtn);

      // Assemble full UI
      dynamicContent.appendChild(polySection);
      dynamicContent.appendChild(listSection);

      // Store references
      window.wmePifElements = {
        list: listContainer,
        count: tabPane.querySelector('#wme-pif-count')
      };

      renderPolygonList(polygonList);
      updateUI();
    }

    // Power button on the tabLabel (exact JAI pattern). Clicking the icon toggles
    // scriptEnabled, updates icon color, (re)creates or removes layers, and swaps pane content.
    tabLabel.addEventListener('click', (e) => {
      if (e.target && e.target.id === 'wme-pif-power-btn') {
        const currentlyEnabled = isScriptEnabled();
        const newState = !currentlyEnabled;

        setScriptEnabled(newState);

        const btn = tabLabel.querySelector('#wme-pif-power-btn');
        if (btn) {
          btn.style.color = newState ? '#4CAF50' : '#888';
        }

        if (newState) {
          loadPolygons();
          for (const poly of polygons.values()) {
            createPolygonLayer(poly);
          }
          renderFullUI();
        } else {
          for (const p of polygons.values()) {
            try {
              wmeSdk.Map.removeLayer({ layerName: p.layerName });
              wmeSdk.LayerSwitcher.removeLayerCheckbox({ name: p.name });
            } catch (_) {}
          }
          renderDisabledUI();
        }
        e.stopPropagation();
      }
    });

    // Initial render based on current state + set correct power icon color on the tab
    const initialPowerBtn = tabLabel.querySelector('#wme-pif-power-btn');
    if (isScriptEnabled()) {
      if (initialPowerBtn) initialPowerBtn.style.color = '#4CAF50';
      renderFullUI();
    } else {
      if (initialPowerBtn) initialPowerBtn.style.color = '#888';
      renderDisabledUI();
    }

    tabPane.appendChild(container);
  }

  function renderPolygonList(container) {
    clearElement(container);

    if (polygons.size === 0) {
      const empty = document.createElement('li');
      empty.textContent = 'No polygons added yet.';
      empty.style.color = '#888';
      container.appendChild(empty);
      return;
    }

    for (const poly of polygons.values()) {
      const li = document.createElement('li');
      li.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;';

      const nameSpan = document.createElement('span');
      nameSpan.textContent = poly.name;
      nameSpan.style.flex = '1';

      const removeBtn = createWzButton('Remove', 'text', () => {
        removePolygon(poly.id);
      });
      removeBtn.style.fontSize = '11px';

      li.appendChild(nameSpan);
      li.appendChild(removeBtn);
      container.appendChild(li);
    }
  }

  function addNewPolygon(name, wkt) {
    const id = 'poly_' + Date.now();
    const layerName = `wme-pif-${id}`;

    const poly = { id, name, wkt, layerName };
    polygons.set(id, poly);

    const success = createPolygonLayer(poly);
    if (success) {
      savePolygons();
      const listEl = document.getElementById('wme-pif-polygon-list');
      if (listEl) renderPolygonList(listEl);
      refreshIssueList();
    } else {
      polygons.delete(id);
    }
  }

  function removePolygon(id) {
    const poly = polygons.get(id);
    if (!poly) return;

    try {
      wmeSdk.Map.removeLayer({ layerName: poly.layerName });
      wmeSdk.LayerSwitcher.removeLayerCheckbox({ name: poly.name });
    } catch (_) {}

    polygons.delete(id);
    savePolygons();

    const listEl = document.getElementById('wme-pif-polygon-list');
    if (listEl) renderPolygonList(listEl);
    refreshIssueList();
  }

  function createWzButton(text, color, onClick) {
    const btn = document.createElement('wz-button');
    btn.setAttribute('size', 'sm');
    btn.setAttribute('color', color);
    btn.textContent = text;
    btn.addEventListener('click', (e) => {
      e.target.blur();
      onClick();
    });
    return btn;
  }

  function updateUI() {
    const els = window.wmePifElements;
    if (!els) return;

    if (els.count) {
      els.count.textContent = `${polygons.size} polygon(s) loaded`;
    }
  }



  // Diagnostic function - draws a small square near the current map center
  // ==================== PUR FILTERING ====================

  /**
   * Returns an array of venues that have PURs and whose representative point
   * lies inside at least one of the user's active polygons.
   */
  function getPURVenuesInPolygons() {
    if (polygons.size === 0) return [];

    const results = [];
    const venues = wmeSdk.DataModel.Venues.getAll();

    for (const venue of venues) {
      const purs = venue.venueUpdateRequests;
      if (!purs || purs.length === 0) continue;

      const point = getVenuePoint(venue);
      if (!point) continue;

      let isInside = false;
      for (const poly of polygons.values()) {
        const polyGeom = parseSimpleWktPolygon(poly.wkt);
        if (polyGeom && pointInPolygon(point, polyGeom)) {
          isInside = true;
          break;
        }
      }

      if (isInside) {
        const purDialogCandidates = getCandidateIdsForPurDialog(venue, purs);
        results.push({
          venue,
          purs,
          purDialogCandidates,
          oldestAge: Math.max(...purs.map(p => p.age || 0))
        });
      }
    }

    // Sort by oldest PUR first
    results.sort((a, b) => b.oldestAge - a.oldestAge);
    return results;
  }

  function refreshIssueList() {
    console.log(`[${SCRIPT_NAME}] Refresh button clicked - re-scanning current view...`);

    const els = window.wmePifElements;
    if (!els) {
      console.warn(`[${SCRIPT_NAME}] refreshIssueList: wmePifElements not found`);
      return;
    }

    const purVenues = getPURVenuesInPolygons();

    clearElement(els.list);

    if (purVenues.length === 0) {
      const empty = document.createElement('div');
      empty.style.cssText = 'padding: 12px; color: #888; font-size: 13px;';
      empty.textContent = polygons.size === 0 
        ? 'Add at least one polygon to start filtering PURs.'
        : 'No PURs found inside your polygons in the current map view.';
      els.list.appendChild(empty);
    } else {
      for (const item of purVenues) {
        const div = document.createElement('div');
        const hasCandidates = item.purDialogCandidates && item.purDialogCandidates.length > 0;
        div.style.cssText = `padding: 8px 10px; border-bottom: 1px solid #eee; ${hasCandidates ? 'cursor: pointer;' : 'opacity: 0.6;'}`;

        const name = document.createElement('div');
        name.style.fontWeight = 'bold';
        name.textContent = item.venue.name || '(Unnamed Venue)';

        // Build richer info line using the PUR data we already have
        const info = document.createElement('div');
        info.style.cssText = 'font-size: 12px; color: #555;';

        const purCount = `${item.purs.length} PUR${item.purs.length > 1 ? 's' : ''}`;
        const oldest = item.oldestAge ? ` • oldest ~${Math.round(item.oldestAge / 86400)}d` : '';

        // Show the most common update types if available
        const types = new Set(item.purs.map(p => p.updateType).filter(Boolean));
        const typeStr = types.size > 0 ? ` • ${Array.from(types).join(', ')}` : '';

        info.textContent = `${purCount}${oldest}${typeStr}${!hasCandidates ? ' (dialog unavailable on this build)' : ''}`;

        div.appendChild(name);
        div.appendChild(info);

        if (hasCandidates) {
          div.addEventListener('click', async () => {
            let success = false;

            // 0. At click time, strongly prefer the live venue.id if it now exists
            //    (the SDK populates properties asynchronously on some builds)
            const liveVenueId = item.venue?.id;
            if (liveVenueId) {
              try {
                wmeSdk.DataModel.Venues.showVenueUpdateRequestDialog(liveVenueId);
                success = true;
              } catch (e) {
                console.warn(`[${SCRIPT_NAME}] Live venue.id failed: ${liveVenueId}`, e);
              }
            }

            // 1. Try all the candidate IDs with the dedicated dialog method
            if (!success) {
              for (const candidateId of item.purDialogCandidates) {
                try {
                  wmeSdk.DataModel.Venues.showVenueUpdateRequestDialog(candidateId);
                  success = true;
                  break;
                } catch (e) {
                  console.warn(`[${SCRIPT_NAME}] showVenueUpdateRequestDialog failed with id=${candidateId}`, e);
                }
              }
            }

            // 2. Strong fallback used by advanced scripts (e.g. UR-MP Tracking patterns):
            //    Prefer using the actual venue object we already have from the data model.
            if (!success && item.venue) {
              try {
                // Re-resolve the venue object if possible (some builds like this more)
                let venueObj = item.venue;
                for (const cid of item.purDialogCandidates) {
                  try {
                    const resolved = wmeSdk.DataModel.Venues.getById?.(cid);
                    if (resolved) {
                      venueObj = resolved;
                      break;
                    }
                  } catch (_) {}
                }

                if (wmeSdk.Selection && typeof wmeSdk.Selection.select === 'function') {
                  wmeSdk.Selection.select({ object: venueObj });
                  success = true;
                }
              } catch (e) {
                console.warn(`[${SCRIPT_NAME}] Selection.select fallback failed`, e);
              }
            }

            // 3. Last resort: old W.model (still works on many builds)
            if (!success && item.venue) {
              try {
                if (window.W && W.model && W.model.venues) {
                  // Try with the first good candidate ID or the object we have
                  const oldId = liveVenueId || item.purDialogCandidates[0] || item.venue.id;
                  const oldVenue = W.model.venues.getObjectById?.(oldId) || item.venue;
                  if (oldVenue && typeof oldVenue.select === 'function') {
                    oldVenue.select();
                    success = true;
                  }
                }
              } catch (e) {
                console.warn(`[${SCRIPT_NAME}] Old W.model fallback failed`, e);
              }
            }

            // 4. Nuclear options - try passing the raw objects directly
            if (!success && item.venue) {
              try {
                if (typeof wmeSdk.DataModel.Venues.showVenueUpdateRequestDialog === 'function') {
                  wmeSdk.DataModel.Venues.showVenueUpdateRequestDialog(item.venue);
                  success = true;
                }
              } catch (e) {
                console.warn(`[${SCRIPT_NAME}] Direct venue object to showVenueUpdateRequestDialog failed`, e);
              }
            }

            if (!success && item.purs && item.purs.length > 0) {
              try {
                const firstPUR = item.purs[0];
                if (firstPUR && typeof wmeSdk.DataModel.Venues.showVenueUpdateRequestDialog === 'function') {
                  wmeSdk.DataModel.Venues.showVenueUpdateRequestDialog(firstPUR);
                  success = true;
                }
              } catch (e) {
                console.warn(`[${SCRIPT_NAME}] Direct PUR object to showVenueUpdateRequestDialog failed`, e);
              }
            }

            if (!success) {
              console.error(`[${SCRIPT_NAME}] All methods failed for PUR dialog.`, {
                candidates: item.purDialogCandidates,
                venue: item.venue,
                samplePUR: item.purs?.[0],
                venueKeys: item.venue ? Object.keys(item.venue) : null,
                purKeys: item.purs?.[0] ? Object.keys(item.purs[0]) : null,
                liveVenueIdAtClick: liveVenueId
              });

              // On this WME build, the SDK does not support programmatically opening
              // the PUR dialog for these venues (even when passing live objects).
              // Make the row clearly indicate the limitation.
              showSnackbar('PUR dialog cannot be opened on this WME build (SDK limitation). Search for the venue manually.', 'warning');

              // As a small convenience, copy the venue name to clipboard
              try {
                const name = item.venue?.name || '(Unnamed)';
                navigator.clipboard?.writeText(name).catch(() => {});
              } catch (_) {}
            }
          });
        } else {
          div.title = 'Venue ID not available in current WME/SDK build';
        }

        els.list.appendChild(div);
      }
    }

    if (els.count) {
      els.count.textContent = `${purVenues.length} PUR venue${purVenues.length !== 1 ? 's' : ''}`;
    }

    console.log(`[${SCRIPT_NAME}] Refresh complete. Found ${purVenues.length} matching venue(s) in current view.`);
  }

  // ==================== INITIALIZATION ====================
  function createPolygonLayer(poly) {
    const { id, name, wkt, layerName } = poly;

    // Remove if it somehow already exists
    try { wmeSdk.Map.removeLayer({ layerName }); } catch (_) {}
    try { wmeSdk.LayerSwitcher.removeLayerCheckbox({ name: name }); } catch (_) {}

    try {
      const geometry = parseSimpleWktPolygon(wkt);
      if (!geometry) {
        console.warn(`[${SCRIPT_NAME}] Skipping invalid WKT for "${name}"`);
        return false;
      }

      const feature = {
        id: `${layerName}_feature`,
        type: 'Feature',
        geometry: geometry,
        properties: { name }
      };

      wmeSdk.Map.addLayer({
        layerName,
        styleRules: [
          {
            predicate: () => true,
            style: {
              strokeColor: '#ff0000',
              strokeOpacity: 0.9,
              strokeWidth: 4,
              fillColor: '#ffaa00',
              fillOpacity: 0.25,
            },
          },
        ],
        styleContext: {},
      });

      wmeSdk.Map.setLayerVisibility({ layerName, visibility: true });
      wmeSdk.LayerSwitcher.addLayerCheckbox({ name: name, isChecked: true });

      wmeSdk.Map.addFeatureToLayer({ feature, layerName });

      console.log(`[${SCRIPT_NAME}] Created layer for polygon "${name}"`);
      return true;
    } catch (e) {
      console.error(`[${SCRIPT_NAME}] Failed to create layer for "${name}":`, e);
      return false;
    }
  }

  async function init() {
    // Get current user for storage namespacing
    const userInfo = wmeSdk.State.userInfo;
    userName = userInfo?.userName || userInfo?.id || 'default';

    // Always create the sidebar tab so user can enable the script
    await createSidebarTab();

    if (!isScriptEnabled()) {
      console.log(`[${SCRIPT_NAME}] Script is currently disabled.`);
      // The tab will show a disabled state with the toggle
      return;
    }

    loadPolygons();

    // Recreate all saved polygon layers
    for (const poly of polygons.values()) {
      createPolygonLayer(poly);
    }

    // Initial PUR list population
    refreshIssueList();

    // Refresh PUR list when map data changes
    wmeSdk.Events.on({
      eventName: 'wme-map-data-loaded',
      eventHandler: () => {
        if (isScriptEnabled()) {
          refreshIssueList();
        }
      }
    });

    // Show initial status
    console.log(`[${SCRIPT_NAME}] v${SCRIPT_VERSION} initialized. Polygons loaded:`, polygons.size);

    // Optional: show a one-time info message on first run
    if (!localStorage.getItem(getStorageKey('seen_welcome'))) {
      setTimeout(() => {
        console.log(`[${SCRIPT_NAME}] Ready. Open the "Polygon Issues" tab in the left sidebar.`);
        localStorage.setItem(getStorageKey('seen_welcome'), '1');
      }, 1500);
    }
  }

  // Start the script
  init().catch(err => {
    console.error(`[${SCRIPT_NAME}] Initialization failed:`, err);
  });
})();