WME Utils - SDK Google Link Enhancer

Adds some extra WME functionality related to Google place links.

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greatest.deepsurf.us/scripts/569692/1789860/WME%20Utils%20-%20SDK%20Google%20Link%20Enhancer.js

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

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

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

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

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

"use strict";
// ==UserScript==
// @name         WME Utils - SDK Google Link Enhancer
// @namespace    WazeDev
// @version      2026.03.26.1
// @description  Adds some extra WME functionality related to Google place links.
// @author       WazeDev group
// @include      /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/
// @license      GNU GPLv3
// ==/UserScript==

/* global google */
/* eslint-disable max-classes-per-file */
const SDKGoogleLinkEnhancer = (() => {
    "use strict";
    var _a;
    class GooglePlaceCache {
        cache;
        pendingPromises;
        constructor() {
            this.cache = new Map();
            this.pendingPromises = new Map();
        }
        async getPlace(placeId) {
            if (this.cache.has(placeId)) {
                return this.cache.get(placeId);
            }
            if (!this.pendingPromises.has(placeId)) {
                let resolveFn;
                let rejectFn;
                const promise = new Promise((resolve, reject) => {
                    resolveFn = resolve;
                    rejectFn = reject;
                    // Set a timeout to reject the promise if not resolved in 3 seconds
                    setTimeout(() => {
                        if (this.pendingPromises.has(placeId)) {
                            this.pendingPromises.delete(placeId);
                            rejectFn(new Error(`Timeout: Place ${placeId} not found within 3 seconds`));
                        }
                    }, 3000);
                });
                this.pendingPromises.set(placeId, { promise, resolve: resolveFn, reject: rejectFn });
            }
            return this.pendingPromises.get(placeId).promise;
        }
        addPlace(placeId, properties) {
            this.cache.set(placeId, properties);
            if (this.pendingPromises.has(placeId)) {
                this.pendingPromises.get(placeId).resolve(properties);
                this.pendingPromises.delete(placeId);
            }
        }
    }
    class GLE {
        #DISABLE_CLOSED_PLACES = false; // Set to TRUE if the feature needs to be temporarily disabled, e.g. during the COVID-19 pandemic.
        #EXT_PROV_ELEM_QUERY = "wz-list-item.external-provider";
        #EXT_PROV_ELEM_CONTENT_QUERY = "div.external-provider-content";
        linkCache;
        #enabled = false;
        #venueChangeHandler = null;
        #mapLayer; // Set in constructor via options.layerName; defaults to "Google Link Enhancements"
        static #debug = false; // Set to true to enable verbose console logging
        #distanceLimit = 400; // Default distance (meters) when Waze place is flagged for being too far from Google place.
        // Area place is calculated as #distanceLimit + <distance between centroid and furthest node>
        #showTempClosedPOIs = true;
        #ptFeature;
        #lineFeature;
        #timeoutID = -1;
        #processDebounceID = -1;          // debounce timer handle for #processPlaces
        #prefetchQueue = [];               // IDs waiting to be dispatched to Google API
        #prefetchInflight = 0;             // count of in-flight getDetails requests
        static #PREFETCH_CONCURRENCY = 50; // max simultaneous getDetails calls
        strings = {
            permClosedPlace: "Google indicates this place is permanently closed.\nVerify with other sources or your editor community before deleting.",
            tempClosedPlace: "Google indicates this place is temporarily closed.",
            multiLinked: "Linked more than once already. Please find and remove multiple links.",
            linkedToThisPlace: "Already linked to this place",
            linkedNearby: "Already linked to a nearby place",
            linkedToXPlaces: "This is linked to {0} places",
            badLink: "Invalid Google link. Please remove it.",
            tooFar: "The Google linked place is more than {0} meters from the Waze place.  Please verify the link is correct.",
        };
        #styleConfig = {
            styleContext: {
                highNodeColor: (context) => {
                    return context?.feature?.properties?.style?.strokeColor;
                },
                strokeColor: (context) => {
                    return context?.feature?.properties?.style?.strokeColor;
                },
                strokeWidth: (context) => {
                    return context?.feature?.properties?.style?.strokeWidth;
                },
                strokeDashStyle: (context) => {
                    return context?.feature?.properties?.style?.strokeDashStyle;
                },
                label: (context) => {
                    return context?.feature?.properties?.style?.label;
                },
                labelYOffset: (context) => {
                    return context?.feature?.properties?.style?.labelYOffset;
                },
                fontColor: (context) => {
                    return context?.feature?.properties?.style?.fontColor;
                },
                fontWeight: (context) => {
                    return context?.feature?.properties?.style?.fontWeight;
                },
                labelOutlineColor: (context) => {
                    return context?.feature?.properties?.style?.labelOutlineColor;
                },
                labelOutlineWidth: (context) => {
                    return context?.feature?.properties?.style?.labelOutlineWidth;
                },
                fontSize: (context) => {
                    return context?.feature?.properties?.style?.fontSize;
                },
                pointRadius: (context) => {
                    return context?.feature?.properties?.style?.pointRadius;
                },
            },
            styleRules: [
                {
                    predicate: (properties) => {
                        return properties.styleName === "lineStyle";
                    },
                    style: {
                        strokeColor: "${strokeColor}",
                    },
                },
                {
                    predicate: (properties) => {
                        return properties.styleName === "default";
                    },
                    style: {
                        strokeColor: "${strokeColor}",
                        strokeWidth: "${strokeWidth}",
                        strokeDashstyle: "${strokeDashstyle}",
                        pointRadius: 15,
                        fillOpacity: 0,
                    },
                },
                {
                    predicate: (properties) => {
                        return properties.styleName === "venueStyle";
                    },
                    style: {
                        strokeColor: "${strokeColor}",
                        strokeWidth: "${strokeWidth}",
                        pointRadius: "${pointRadius}",
                        fillOpacity: 0,
                    },
                },
                {
                    predicate: (properties) => {
                        return properties.styleName === "placeStyle";
                    },
                    style: {
                        strokeColor: "${strokeColor}",
                        strokeWidth: "${strokeWidth}",
                        strokeDashStyle: "${strokeDashStyle}",
                        pointRadius: "${pointRadius}",
                        fillOpacity: 0,
                    },
                },
                {
                    predicate: (properties) => {
                        return properties.styleName === "googlePlacePointStyle";
                    },
                    style: {
                        pointRadius: "${pointRadius}",
                        strokeWidth: "${strokeWidth}",
                        strokeColor: "${strokeColor}",
                        fillColor: "${fillColor}",
                        strokeOpacity: "${strokeOpacity}",
                    },
                },
                {
                    predicate: (properties) => {
                        return properties.styleName === "googlePlaceLineStyle";
                    },
                    style: {
                        strokeWidth: "${strokeWidth}",
                        strokeDashstyle: "${strokeDashStyle}",
                        strokeColor: "${strokeColor}",
                        label: "${label}",
                        labelYOffset: "${labelYOffset}",
                        fontColor: "${fontColor}",
                        fontWeight: "${fontWeight}",
                        labelOutlineColor: "${labelOutlineColor}",
                        labelOutlineWidth: "${labelOutlineWidth}",
                        fontSize: "${fontSize}",
                    }
                }
            ],
        };
        sdk;
        trf;
        constructor(sdk, trf, { layerName = "Google Link Enhancements" } = {}) {
            let msg = "";
            if (!sdk) {
                msg += "SDK Must be defined to use GLE";
            }
            if (!trf) {
                msg += "\n";
                msg += "Turf Library Must be made available to GLE to Implement Some of the Functionality";
            }
            this.sdk = sdk;
            this.trf = trf;
            this.#mapLayer = layerName;
            this.linkCache = new GooglePlaceCache();
            this.#initLayer();
            this.sdk.Events.on({
                eventName: "wme-map-data-loaded",
                eventHandler: () => {
                    this.#processPlaces();
                }
            });
            this.sdk.Events.on({
                eventName: "wme-data-model-objects-added",
                eventHandler: (payload) => {
                    if (payload.dataModelName === "venues") {
                        this.#processPlaces();
                    }
                }
            });
            this.sdk.Events.on({
                eventName: "wme-data-model-objects-removed",
                eventHandler: (payload) => {
                    if (payload.dataModelName === "venues") {
                        this.#processPlaces();
                    }
                },
            });
            this.sdk.Events.on({
                eventName: "wme-data-model-objects-changed",
                eventHandler: (payload) => {
                    if (payload.dataModelName === "venues") {
                        this.#processPlaces();
                    }
                }
            });
            // This is a special event that will be triggered when DOM elements are destroyed.
            /* eslint-disable wrap-iife, func-names, object-shorthand */
            (($) => {
                $.event.special.destroyed = {
                    remove: (o) => {
                        if (o.handler && o.type !== "destroyed") {
                            o.handler();
                        }
                    },
                };
            })(jQuery);
            /* eslint-enable wrap-iife, func-names, object-shorthand */
            // In case a place is already selected on load.
            let currentSelection;
            try { currentSelection = this.sdk.Editing.getSelection(); } catch (e) { currentSelection = null; }
            if (currentSelection?.ids?.length && currentSelection.objectType === "venue") {
                this.#formatLinkElements();
            }
            this.sdk.Events.on({
                eventName: "wme-selection-changed",
                eventHandler: this.#onWmeSelectionChanged.bind(this),
            });
            this.sdk.Events.on({
                eventName: "wme-layer-checkbox-toggled",
                eventHandler: (payload) => {
                    if (payload.name === this.#mapLayer) {
                        if (payload.checked) {
                            // Layer re-shown: redraw rings from cache (no new API calls needed).
                            this.#processPlaces();
                        } else {
                            // Layer hidden: clear features but keep GLE running so sidebar
                            // colouring, hover line, and API interception continue to work.
                            this.sdk.Map.removeAllFeaturesFromLayer({ layerName: this.#mapLayer });
                        }
                    }
                },
            });
        }
        #initLayer() {
            this.sdk.Map.addLayer({
                layerName: this.#mapLayer,
                styleContext: this.#styleConfig.styleContext,
                styleRules: this.#styleConfig.styleRules,
            });
            this.sdk.Map.setLayerOpacity({ layerName: this.#mapLayer, opacity: 0.8 });
            this.sdk.LayerSwitcher.addLayerCheckbox({ name: this.#mapLayer, isChecked: true });
        }
        #onWmeSelectionChanged() {
            if (this.#enabled) {
                this.#destroyPoint();
                let selected;
                try { selected = this.sdk.Editing.getSelection(); } catch (e) { return; }
                if (selected?.objectType === "venue") {
                    // The setTimeout is necessary (in beta WME currently, at least) to allow the
                    // panel UI DOM to update after a place is selected.
                    setTimeout(() => this.#formatLinkElements(), 0);
                }
            }
        }
        enable() {
            if (!this.#enabled) {
                this.#interceptGooglePlacesAPIs();
                $("#map").on("mouseenter", null, this, _a.#onMapMouseenter);
                this.#venueChangeHandler = (payload) => {
                    if (payload.dataModelName === "venues") {
                        this.#formatLinkElements();
                    }
                };
                this.sdk.Events.on({
                    eventName: "wme-data-model-objects-changed",
                    eventHandler: this.#venueChangeHandler,
                });
                this.#enabled = true;
                this.#processPlaces();
            }
        }
        disable() {
            if (this.#enabled) {
                $("#map").off("mouseenter", _a.#onMapMouseenter);
                if (this.#venueChangeHandler) {
                    this.sdk.Events.off({
                        eventName: "wme-data-model-objects-changed",
                        eventHandler: this.#venueChangeHandler,
                    });
                    this.#venueChangeHandler = null;
                }
                if (this.#processDebounceID !== -1) {
                    clearTimeout(this.#processDebounceID);
                    this.#processDebounceID = -1;
                }
                this.#enabled = false;
                this.sdk.Map.removeAllFeaturesFromLayer({ layerName: this.#mapLayer });
            }
        }
        // The distance (in meters) before flagging a Waze place that is too far from the linked Google place.
        // Area places use distanceLimit, plus the distance from the centroid of the AP to its furthest node.
        get distanceLimit() {
            return this.#distanceLimit;
        }
        set distanceLimit(value) {
            this.#distanceLimit = value;
        }
        get showTempClosedPOIs() {
            return this.#showTempClosedPOIs;
        }
        set showTempClosedPOIs(value) {
            this.#showTempClosedPOIs = value;
            this.#processPlaces();
        }
        #distanceBetweenPoints(point1, point2) {
            const ls = this.trf.lineString([point1, point2]);
            const length = this.trf.length(ls);
            return length * 1000; // multiply by 3.28084 to convert to feet
        }
        static isPointVenue(venue) {
            return venue.geometry.type === "Point";
        }
        #isLinkTooFar(link, venue) {
            if (link.loc) {
                const linkPt = this.trf.point([link.loc.lng, link.loc.lat]);
                let venuePt;
                let distanceLim = this.distanceLimit;
                if (venue.geometry.type === "Point") {
                    venuePt = venue.geometry;
                }
                else {
                    const center = this.trf.centroid(venue.geometry);
                    venuePt = center.geometry;
                    let bbox = venue.geometry.bbox;
                    if (!bbox) {
                        bbox = this.trf.bbox(venue.geometry);
                    }
                    const topRightPt = this.trf.point([bbox[0], bbox[1]]);
                    distanceLim += this.#distanceBetweenPoints(venuePt.coordinates, topRightPt.geometry.coordinates);
                }
                const distance = this.#distanceBetweenPoints(linkPt.geometry.coordinates, venuePt.coordinates);
                return distance > distanceLim;
            }
            return false;
        }
        #processPlaces() {
            if (!this.#enabled) return;
            if (this.#processDebounceID !== -1) clearTimeout(this.#processDebounceID);
            this.#processDebounceID = setTimeout(() => {
                this.#processDebounceID = -1;
                this.#doProcessPlaces();
            }, 150);
        }
        #doProcessPlaces() {
            try {
                    // Only draw map rings when the layer is visible in the Map Layers panel.
                    // Always clear first (even when hidden) so in-flight API responses can't
                    // ghost features back onto a layer the user has just hidden.
                    // Prefetch always runs so the cache stays warm and rings reappear instantly
                    // when the layer is re-shown, without needing new API calls.
                    const layerVisible = this.sdk.LayerSwitcher.isLayerCheckboxChecked({ name: this.#mapLayer });
                    // Get a list of already-linked id's
                    const existingLinks = SDKGoogleLinkEnhancer.#getExistingLinks(this.sdk);
                    this.sdk.Map.removeAllFeaturesFromLayer({ layerName: this.#mapLayer });
                    const drawnLinks = [];
                    // Clear stale queue entries from previous viewport before building uncachedIds.
                    this.#prefetchQueue = [];
                    const objectIds = [];
                    for (const venue of this.sdk.DataModel.Venues.getAll()) {
                        objectIds.push(venue.id);
                    }
                    const uncachedIds = new Set();
                    for (const objId of objectIds) {
                        const promises = [];
                        const venue = this.sdk.DataModel.Venues.getById({ venueId: objId.toString() });
                        if (venue === null)
                            continue;
                        for (const provID of venue.externalProviderIds) {
                            const id = provID;
                            if (!this.linkCache.cache.has(id) && !this.linkCache.pendingPromises.has(id)) {
                                uncachedIds.add(id);
                            }
                            // Check for duplicate links
                            const linkInfo = existingLinks[id];
                            if (linkInfo?.count > 1) {
                                const geometry = venue.geometry;
                                const width = _a.isPointVenue(venue) ? 4 : 12;
                                const color = "#fb8d00";
                                const features = [
                                    geometry.type === "Point"
                                        ? this.trf.point(geometry.coordinates, {
                                            styleName: "venueStyle",
                                            style: {
                                                strokeWidth: width,
                                                strokeColor: color,
                                                pointRadius: 15,
                                            },
                                        }, { id: `venue_${geometry.toString()}` })
                                        : this.trf.polygon(geometry.coordinates, {
                                            styleName: "venueStyle",
                                            style: {
                                                strokeColor: color,
                                                strokeWidth: width,
                                            },
                                        }, { id: `polyvenue_${geometry.toString()}` }),
                                ];
                                const lineStart = this.trf.centroid(geometry);
                                for (const linkVenue of linkInfo.venues) {
                                    if (linkVenue !== venue &&
                                        !drawnLinks.some((dl) => (dl[0] === venue && dl[1] === linkVenue) ||
                                            (dl[0] === linkVenue && dl[1] === venue))) {
                                        const endPoint = this.trf.centroid(linkVenue.geometry);
                                        features.push(this.trf.lineString([lineStart.geometry.coordinates, endPoint.geometry.coordinates], {
                                            styleName: "lineStyle",
                                            style: {
                                                strokeWidth: 4,
                                                strokeColor: color,
                                                strokeDashstyle: "12 12",
                                            },
                                        }, { id: `ls_${lineStart.geometry.toString()}_${endPoint.geometry.toString()}` }));
                                        drawnLinks.push([venue, linkVenue]);
                                    }
                                }
                                if (layerVisible) {
                                    this.sdk.Map.addFeaturesToLayer({ features: features, layerName: this.#mapLayer });
                                }
                            }
                            promises.push(this.linkCache.getPlace(id));
                        }
                        // Process all results of link lookups and add a highlight feature if needed.
                        Promise.all(promises).then((results) => {
                            let strokeColor = null;
                            let strokeDashStyle = "solid";
                            if (!this.#DISABLE_CLOSED_PLACES && results.some((res) => res.permclosed)) {
                                if (/^(\[|\()?(permanently )?closed(\]|\)| -)/i.test(venue.name) ||
                                    /(\(|- |\[)(permanently )?closed(\)|\])?$/i.test(venue.name)) {
                                    strokeDashStyle = _a.isPointVenue(venue) ? "2 6" : "2 16";
                                }
                                strokeColor = "#F00";
                            }
                            else if (results.some((res) => this.#isLinkTooFar(res, venue))) {
                                strokeColor = "#0FF";
                            }
                            else if (!this.#DISABLE_CLOSED_PLACES &&
                                this.#showTempClosedPOIs &&
                                results.some((res) => res.tempclosed)) {
                                if (/^(\[|\()?(temporarily )?closed(\]|\)| -)/i.test(venue.name) ||
                                    /(\(|- |\[)(temporarily )?closed(\)|\])?$/i.test(venue.name)) {
                                    strokeDashStyle = _a.isPointVenue(venue) ? "2 6" : "2 16";
                                }
                                strokeColor = "#FD3";
                            }
                            else if (results.some((res) => res.notFound)) {
                                strokeColor = "#F0F";
                            }
                            if (strokeColor && layerVisible) {
                                const style = {
                                    strokeWidth: _a.isPointVenue(venue) ? 4 : 12,
                                    strokeColor,
                                    strokeDashStyle,
                                    pointRadius: 15,
                                };
                                const feature = _a.isPointVenue(venue)
                                    ? this.trf.point(venue.geometry.coordinates, {
                                        styleName: "placeStyle",
                                        style: style,
                                    }, { id: `place_${venue.id}` })
                                    : this.trf.polygon(venue.geometry.coordinates, {
                                        styleName: "placeStyle",
                                        style: style,
                                    }, { id: `place_${venue.id}` });
                                this.sdk.Map.addFeaturesToLayer({ features: [feature], layerName: this.#mapLayer });
                            }
                        }).catch(() => {
                            // Timeout: Google hasn't called getDetails for this venue yet — suppress silently.
                        });
                    }
                    // Proactively fetch Google data for venues not yet in cache.
                    // When responses arrive the interceptor populates the cache and
                    // triggers another #processPlaces pass to render the highlights.
                    // Skip prefetch when the layer is hidden — sidebar coloring works
                    // reactively via the interceptor, so there's no benefit fetching
                    // data for rings that aren't being displayed.
                    if (layerVisible) {
                        this.#prefetchPlaceData([...uncachedIds]);
                    }
                }
                catch (ex) {
                    console.error("PIE (Google Link Enhancer) error:", ex);
                }
        }
        // Proactively fetches Google place data for each uncached place ID so that
        // GLE can highlight closed/far venues in the viewport without requiring user selection.
        // Always uses the legacy PlacesService.getDetails — the new Place.fetchFields API calls
        // places.googleapis.com which is not in WME's Content Security Policy connect-src list
        // and will be blocked by the browser. The Place.fetchFields interceptor in
        // #interceptGooglePlacesAPIs remains active in case WME itself ever migrates to the new API.
        // Requests are capped at #PREFETCH_CONCURRENCY simultaneous in-flight calls; the rest
        // are queued and drained automatically as each response arrives.
        #prefetchPlaceData(placeIds) {
            if (!placeIds.length) return;
            if (typeof google === "undefined" || !google.maps?.places?.PlacesService) return;
            for (const id of placeIds) {
                if (!this.#prefetchQueue.includes(id)) {
                    this.#prefetchQueue.push(id);
                }
            }
            this.#drainPrefetchQueue();
        }
        #drainPrefetchQueue() {
            if (this.#prefetchInflight >= SDKGoogleLinkEnhancer.#PREFETCH_CONCURRENCY
                || this.#prefetchQueue.length === 0) return;
            if (typeof google === "undefined" || !google.maps?.places?.PlacesService) return;
            const service = new google.maps.places.PlacesService(document.createElement("div"));
            while (this.#prefetchInflight < SDKGoogleLinkEnhancer.#PREFETCH_CONCURRENCY
                   && this.#prefetchQueue.length > 0) {
                const placeId = this.#prefetchQueue.shift();
                this.#prefetchInflight++;
                service.getDetails(
                    { placeId, fields: ["place_id", "geometry", "business_status"] },
                    () => {
                        // Interceptor in #interceptGooglePlacesAPIs handles cache population
                        // and triggers #processPlaces(). This callback only manages queue state.
                        this.#prefetchInflight--;
                        this.#drainPrefetchQueue();
                    }
                );
            }
        }
        static #onMapMouseenter(event) {
            // If the point isn't destroyed yet, destroy it when mousing over the map.
            event.data.#destroyPoint();
        }
        async #formatLinkElements() {
            const $links = $("#edit-panel").find(this.#EXT_PROV_ELEM_QUERY);
            if ($links.length) {
                const existingLinks = _a.#getExistingLinks(this.sdk);
                // fetch all links first
                const promises = [];
                const extProvElements = [];
                $links.each((ix, linkEl) => {
                    const $linkEl = $(linkEl);
                    extProvElements.push($linkEl);
                    const id = this.#getIdFromElement($linkEl);
                    promises.push(this.linkCache.getPlace(id));
                });
                const links = await Promise.all(promises);
                extProvElements.forEach(($extProvElem, i) => {
                    const id = this.#getIdFromElement($extProvElem);
                    if (!id)
                        return;
                    const link = links[i];
                    if (existingLinks[id] && existingLinks[id].count > 1 && existingLinks[id].isThisVenue) {
                        setTimeout(() => {
                            $extProvElem
                                .find(this.#EXT_PROV_ELEM_CONTENT_QUERY)
                                .css({ backgroundColor: "#FFA500" })
                                .attr({
                                title: this.strings.linkedToXPlaces.replace("{0}", existingLinks[id].count),
                            });
                        }, 50);
                    }
                    this.#addHoverEvent($extProvElem);
                    if (link) {
                        if (link.permclosed && !this.#DISABLE_CLOSED_PLACES) {
                            $extProvElem
                                .find(this.#EXT_PROV_ELEM_CONTENT_QUERY)
                                .css({ backgroundColor: "#FAA" })
                                .attr("title", this.strings.permClosedPlace);
                        }
                        else if (link.tempclosed && !this.#DISABLE_CLOSED_PLACES) {
                            $extProvElem
                                .find(this.#EXT_PROV_ELEM_CONTENT_QUERY)
                                .css({ backgroundColor: "#FFA" })
                                .attr("title", this.strings.tempClosedPlace);
                        }
                        else if (link.notFound) {
                            $extProvElem
                                .find(this.#EXT_PROV_ELEM_CONTENT_QUERY)
                                .css({ backgroundColor: "#F0F" })
                                .attr("title", this.strings.badLink);
                        }
                        else {
                            let selection;
                            try { selection = this.sdk.Editing.getSelection(); } catch (e) { selection = null; }
                            if (selection?.objectType === "venue") {
                                const venue = this.sdk.DataModel.Venues.getById({ venueId: selection.ids[0] });
                                if (venue && this.#isLinkTooFar(link, venue)) {
                                    $extProvElem
                                        .find(this.#EXT_PROV_ELEM_CONTENT_QUERY)
                                        .css({ backgroundColor: "#0FF" })
                                        .attr("title", this.strings.tooFar.replace("{0}", this.distanceLimit.toString()));
                                }
                                else {
                                    // reset in case we just deleted another provider
                                    $extProvElem
                                        .find(this.#EXT_PROV_ELEM_CONTENT_QUERY)
                                        .css({ backgroundColor: "" })
                                        .attr("title", "");
                                }
                            }
                        }
                    }
                });
            }
        }
        static #getExistingLinks(sdk = undefined) {
            if (!sdk) {
                const msg = "SDK Is Not Available";
                console.error(msg);
                throw new Error(msg);
            }
            const existingLinks = {};
            let thisVenue;
            try { thisVenue = sdk.Editing.getSelection(); } catch (e) { return {}; }
            if (thisVenue?.objectType !== "venue")
                return {};
            for (const venue of sdk.DataModel.Venues.getAll()) {
                const isThisVenue = venue.id === thisVenue.ids[0];
                const thisPlaceIDs = [];
                for (const provID of venue.externalProviderIds) {
                    const id = provID;
                    if (!thisPlaceIDs.includes(id)) {
                        thisPlaceIDs.push(id);
                        let link = existingLinks[id];
                        if (link) {
                            link.count++;
                            link.venues.push(venue);
                        }
                        else {
                            link = { count: 1, venues: [venue] };
                            existingLinks[id] = link;
                        }
                        link.isThisVenue = link.isThisVenue || isThisVenue;
                    }
                }
            }
            return existingLinks;
        }
        // Remove the POI point from the map.
        #destroyPoint() {
            if (this.#ptFeature) {
                this.sdk.Map.removeFeaturesFromLayer({ featureIds: [this.#ptFeature.id, this.#lineFeature.id], layerName: this.#mapLayer });
                this.#ptFeature = null;
                this.#lineFeature = null;
            }
        }
        #getOLMapExtent() {
            return this.sdk.Map.getMapExtent();
        }
        // Add the POI point to the map.
        async #addPoint(id) {
            if (!id)
                return;
            const link = await this.linkCache.getPlace(id);
            if (link) {
                if (!link.notFound) {
                    const coord = link.loc;
                    const poiPt = this.trf.point([coord.lng, coord.lat]);
                    let selection;
                    try { selection = this.sdk.Editing.getSelection(); } catch (e) { selection = null; }
                    let placeGeom;
                    if (selection?.objectType === "venue") {
                        const v = this.sdk.DataModel.Venues.getById({ venueId: selection.ids[0] });
                        placeGeom = v?.geometry && this.trf.centroid(v?.geometry)?.geometry;
                    }
                    else {
                        return;
                    }
                    const placePt = this.trf.point(placeGeom.coordinates);
                    const ext = this.#getOLMapExtent();
                    const lsBounds = this.trf.lineString([
                        [ext[0], ext[3]],
                        [ext[0], ext[1]],
                        [ext[2], ext[1]],
                        [ext[2], ext[3]],
                        [ext[0], ext[3]],
                    ]);
                    let lsLine = this.trf.lineString([placePt.geometry.coordinates, poiPt.geometry.coordinates]);
                    // If the line extends outside the bounds, split it so we don't draw a line across the world.
                    const splits = this.trf.lineSplit(lsLine, lsBounds);
                    let label = "";
                    if (splits) {
                        for (const split of splits.features) {
                            for (const component of split.geometry.coordinates) {
                                if (component[0] === placePt.geometry.coordinates[0] &&
                                    component[1] === placePt.geometry.coordinates[1])
                                    lsLine = split;
                            }
                        }
                        let distance = this.#distanceBetweenPoints(poiPt.geometry.coordinates, placePt.geometry.coordinates);
                        let unitConversion;
                        let unit1;
                        let unit2;
                        if (this.sdk.Settings.getUserSettings().isImperial) {
                            distance *= 3.28084;
                            unitConversion = 5280;
                            unit1 = " ft";
                            unit2 = " mi";
                        }
                        else {
                            unitConversion = 1000;
                            unit1 = " m";
                            unit2 = " km";
                        }
                        if (distance > unitConversion * 10) {
                            label = Math.round(distance / unitConversion) + unit2;
                        }
                        else if (distance > 1000) {
                            label = Math.round(distance / (unitConversion / 10)) / 10 + unit2;
                        }
                        else {
                            label = Math.round(distance) + unit1;
                        }
                    }
                    this.#destroyPoint(); // Just in case it still exists.
                    this.#ptFeature = this.trf.point(poiPt.geometry.coordinates, {
                        styleName: "googlePlacePointStyle",
                        style: {
                            pointRadius: 6,
                            strokeWidth: 30,
                            strokeColor: "#FF0",
                            fillColor: "#FF0",
                            strokeOpacity: 0.5,
                        },
                    }, { id: `PoiPT_${poiPt.toString()}` });
                    this.#lineFeature = this.trf.lineString(lsLine.geometry.coordinates, {
                        styleName: "googlePlaceLineStyle",
                        style: {
                            strokeWidth: 3,
                            strokeDashstyle: "12 8",
                            strokeColor: "#FF0",
                            label,
                            labelYOffset: 45,
                            fontColor: "#FF0",
                            fontWeight: "bold",
                            labelOutlineColor: "#000",
                            labelOutlineWidth: 4,
                            fontSize: "18",
                        },
                    }, { id: `LsLine_${lsLine.toString()}` });
                    this.sdk.Map.addFeaturesToLayer({ features: [this.#ptFeature, this.#lineFeature], layerName: this.#mapLayer });
                    this.#timeoutDestroyPoint();
                }
            }
        }
        // Destroy the point after some time, if it hasn't been destroyed already.
        #timeoutDestroyPoint() {
            if (this.#timeoutID > 0)
                clearTimeout(this.#timeoutID);
            this.#timeoutID = setTimeout(() => this.#destroyPoint(), 4000);
        }
        #getIdFromElement($el) {
            const providerIndex = $el.parent().children().toArray().indexOf($el[0]);
            let selection;
            try { selection = this.sdk.Editing.getSelection(); } catch (e) { return null; }
            if (!selection || selection.objectType !== "venue") return null;
            const venue = this.sdk.DataModel.Venues.getById({ venueId: selection.ids[0] });
            return venue?.externalProviderIds?.[providerIndex] ?? null;
        }
        #addHoverEvent($el) {
            $el.hover(() => this.#addPoint(this.#getIdFromElement($el)), () => this.#destroyPoint());
        }
        #interceptGooglePlacesAPIs() {
            // --- Legacy PlacesService.getDetails interceptor ---
            // Kept for backward compat: WME may still call the legacy API internally.
            if (typeof google === "undefined" ||
                !google.maps ||
                !google.maps.places ||
                !google.maps.places.PlacesService) {
                if (_a.#debug) console.debug("Google Maps PlacesService not loaded yet.");
                setTimeout(this.#interceptGooglePlacesAPIs.bind(this), 500); // Retry until it loads
                return;
            }
            const originalGetDetails = google.maps.places.PlacesService.prototype.getDetails;
            const that = this;
            google.maps.places.PlacesService.prototype.getDetails = function interceptedGetDetails(request, callback) {
                const customCallback = (result, status) => {
                    const link = {};
                    let cacheUpdated = false;
                    switch (status) {
                        case google.maps.places.PlacesServiceStatus.OK: {
                            const loc = result.geometry.location;
                            link.loc = { lng: loc.lng(), lat: loc.lat() };
                            if (result.business_status === google.maps.places.BusinessStatus.CLOSED_PERMANENTLY) {
                                link.permclosed = true;
                                if (_a.#debug) console.debug("GLE: permanently closed place detected:", request.placeId);
                            }
                            else if (result.business_status === google.maps.places.BusinessStatus.CLOSED_TEMPORARILY) {
                                link.tempclosed = true;
                                if (_a.#debug) console.debug("GLE: temporarily closed place detected:", request.placeId);
                            }
                            that.linkCache.addPlace(request.placeId, link);
                            cacheUpdated = true;
                            break;
                        }
                        case google.maps.places.PlacesServiceStatus.NOT_FOUND:
                            link.notFound = true;
                            that.linkCache.addPlace(request.placeId, link);
                            cacheUpdated = true;
                            if (_a.#debug) console.debug("GLE: invalid/not-found Google link:", request.placeId);
                            break;
                        default:
                            link.error = status;
                            if (_a.#debug) console.debug("GLE: unexpected getDetails status:", status, request.placeId);
                    }
                    // Re-render map highlights now that Google data has arrived for this place.
                    // #processPlaces ran earlier (on map load) before the cache was populated,
                    // so a new pass is needed each time a getDetails response comes back.
                    // The debounce inside #processPlaces coalesces rapid back-to-back calls.
                    if (cacheUpdated) {
                        that.#processPlaces();
                    }
                    callback(result, status); // Pass the result to the original callback
                };
                return originalGetDetails.call(this, request, customCallback);
            };
            if (_a.#debug) console.debug("Google Maps PlacesService.getDetails intercepted successfully.");

            // --- New Place.fetchFields interceptor ---
            // Intercepts calls from WME itself if/when it migrates to the new API.
            // Note: #prefetchPlaceData always uses the legacy PlacesService.getDetails because
            // Place.fetchFields calls places.googleapis.com which is blocked by WME's CSP.
            if (!google.maps.places.Place) {
                if (_a.#debug) console.debug("New Place class not available — skipping Place.fetchFields intercept.");
                return;
            }
            const originalFetchFields = google.maps.places.Place.prototype.fetchFields;
            google.maps.places.Place.prototype.fetchFields = async function interceptedFetchFields(options) {
                try {
                    const result = await originalFetchFields.call(this, options);
                    const place = result.place;
                    const link = {};
                    if (place.location) {
                        link.loc = { lng: place.location.lng(), lat: place.location.lat() };
                    }
                    if (place.businessStatus === google.maps.places.BusinessStatus.CLOSED_PERMANENTLY) {
                        link.permclosed = true;
                    } else if (place.businessStatus === google.maps.places.BusinessStatus.CLOSED_TEMPORARILY) {
                        link.tempclosed = true;
                    }
                    that.linkCache.addPlace(this.id, link);
                    that.#processPlaces();
                    return result;
                } catch (err) {
                    // Only cache as notFound for a definitive NOT_FOUND response.
                    // Other errors (CSP block, quota, network) are transient — leave uncached for retry.
                    const status = String(err?.status ?? err?.code ?? err?.message ?? "").toUpperCase();
                    if (status.includes("NOT_FOUND")) {
                        that.linkCache.addPlace(this.id, { notFound: true });
                        that.#processPlaces();
                    }
                    throw err; // Re-throw so WME and other callers still receive the error.
                }
            };
            if (_a.#debug) console.debug("Google Maps Place.fetchFields intercepted successfully.");
        }
    }
    _a = GLE;
    return GLE;
})();