Filter Place Update Requests (PURs) inside custom WKT polygons. No external dependencies. Shows matching PUR venues in the sidebar with toggleable polygon layers.
// ==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);
});
})();