WME JumpMaps Baltics

The script adds in the WME links to third party mapping systems (Google/Open Street Maps/HERE etc.)

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name WME JumpMaps Baltics
// @description The script adds in the WME links to third party mapping systems (Google/Open Street Maps/HERE etc.)
// @license MIT
// @match https://*.waze.com/*editor*
// @match https://www.kadastrs.lv/map/*
// @match https://kartes.lgia.gov.lv/*
// @match https://*.balticmaps.eu/*
// @match https://maps.google.com
// @match https://maps.apple.com
// @match https://wikimapia.org/*
// @match https://www.openstreetmap.org/*
// @match https://mapcam.info/speedcam/*
// @match https://www.mapillary.com/*
// @require https://cdnjs.cloudflare.com/ajax/libs/proj4js/2.11.0/proj4.js
// @require https://unpkg.com/[email protected]/dist/leaflet.js
// @icon data:image/png;base64,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
// @version 8.1.1
// @author N190392
// @namespace https://greasyfork.org/en/scripts/481079-wme-jumpmaps
// ==/UserScript==

/* global W, L, proj4 */

(function () {
    'use strict';

    // ============================================================================
    // CONSTANTS & CONFIG
    // ============================================================================

    const SCRIPT_NAME    = GM_info.script.name;
    const SCRIPT_VERSION = GM_info.script.version;
    const ICON_WME       = GM_info.script.icon;
    const PANEL_ID       = `WME.JumpMaps_${SCRIPT_VERSION}`;

    const LS = {
        DEBUG:           'WMEJumpMapsDebug',
        RESTORE_SEL:     'WMEJumpMapsRestoreSelected',
        HIDE_WINDOW:     'WMEJumpMapsHideWindow',
        TOP_OFFSET:      'WMEJumpMapsTopOffset',
        LEFT_OFFSET:     'WMEJumpMapsLeftOffset',
        CHECKBOX_STATES: 'WMEJumpMapsCheckboxStates',
        SHOW_MINIMAP:    'WMEJumpMapsShowMinimap',
        MINIMAP_TOP:     'WMEJumpMapsMinimapTop',
        MINIMAP_LEFT:    'WMEJumpMapsMinimapLeft',
        FIXED_POS:       'WMEJumpMapsFixedPos',
    };

    const DEFAULT_TOP  = '30px';
    const DEFAULT_MINIMAP_TOP  = null;   // null = use bottom:30px initial positioning
    const DEFAULT_MINIMAP_LEFT = '10px';
    const DEFAULT_LEFT = '0px';

    const LKS92_PROJ = '+proj=tmerc +lat_0=0 +lon_0=24 +k=0.9996 +x_0=500000 +y_0=-6000000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs';
    const LKS94_PROJ = '+proj=tmerc +lat_0=0 +lon_0=24 +k=0.9996 +x_0=500000 +y_0=0        +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs';

    // Zoom conversion table (WME zoom → KDL radius)
    const ARR_W2KDL = [
        {w:0,r:75000},{w:1,r:50000},{w:2,r:15000},{w:3,r:10000},{w:4,r:5000},
        {w:5,r:3000},{w:6,r:1000},{w:7,r:750},{w:8,r:500},{w:9,r:200},
    ];

    // ============================================================================
    // STATE
    // ============================================================================

    let debug           = false;
    let restoreSelected = false;
    let hideWindow      = false;
    let fixedPos        = false;
    let topOffset       = DEFAULT_TOP;
    let leftOffset      = DEFAULT_LEFT;
    let showMinimap     = getLS(LS.SHOW_MINIMAP, 'bool', true);
    let minimapInstance = null;
    let initProbeCount  = 0;

    // Config is initialised in bootstrap from CONFIG_DEFAULTS
    let Config = {};

    const CONFIG_DEFAULTS = {
        '_map_WME':    { save:0, title:'Open in WME',                    name:'[WME]',      template:'https://www.waze.com/editor/?env=row&zoomLevel={{zoom}}&lat={{lat}}&lon={{lon}}' },
        '_map_LI':     { save:0, title:'Open in LiveMap',                name:'[Live]',     template:'https://www.waze.com/livemap/?zoom={{zoom}}&lon={{lon}}&lat={{lat}}' },
        '_map_OSM':    { save:1, title:'OpenStreetMap',                  name:'OpenStreetMap', template:'http://www.openstreetmap.org/#map={{zoom}}/{{lat}}/{{lon}}' },
        '_map_Google': { save:1, title:'Google Maps',                    name:'Google',     template:'http://www.google.com/maps/?ll={{lat}}%2C{{lon}}&z={{zoom}}&t=m' },
        '_map_BING':   { save:1, title:'Bing Maps',                      name:'Bing',       template:'http://www.bing.com/maps/?v=2&cp={{lat}}~{{lon}}&lvl={{zoom}}&dir=0&sty=h&form=LMLTEW' },
        '_map_HERE':   { save:1, title:'HERE WeGo',                      name:'HERE',       template:'https://wego.here.com/?map={{lat}},{{lon}},{{zoom}},normal' },
        '_map_APPLE':  { save:1, title:'Apple Maps',                     name:'Apple',      template:'https://maps.apple.com/?ll={{lat}},{{lon}}&spn=0.0038614715299516433%2C0.010368359444299813' },
        '_map_MRY':    { save:1, title:'Mapillary',                      name:'Mapillary',  template:'https://www.mapillary.com/app/?lat={{lat}}&lng={{lon}}&z={{zoom}}' },
        '_map_WM':     { save:1, title:'Wikimapia',                      name:'Wikimapia',  template:'https://wikimapia.org/#lang=ru&lat={{lat}}&lon={{lon}}&z={{zoom}}&m=b' },
        '_map_SC':     { save:1, title:'MapCam',                         name:'MapCam',     template:'https://mapcam.info/speedcam/?lng={{lon}}&lat={{lat}}&z={{zoom}}&t=OSM' },
        '_map_WMFLAB': { save:1, title:'GeoHack',                        name:'GeoHack',    template:'https://geohack.toolforge.org/geohack.php?params={{lat}}_N_{{lon}}_E_scale:{{zoom}}' },
        '_map_OSV':    { save:1, title:'KartaView',                      name:'KartaView',  template:'https://kartaview.org/map/@{{lat}},{{lon}},{{zoom}}z' },
        '_map_RBASE':  { save:1, title:'RadarBase.info',                 name:'RadarBase',  template:'https://radarbase.info/map/actual/{{lat}}/{{lon}}/{{zoom}}' },
        '_map_SPRO':   { save:1, title:'satellites.pro',                 name:'SatPRO',     template:'https://satellites.pro/#{{lat}},{{lon}},{{zoom}}' },
        '_hdr_LV':     { type: 'header',                                 label: 'Latvia',   flag: 'LV' },
        '_map_BM':     { save:1, title:'BalticMaps',                     name:'BalticMaps', template:'https://balticmaps.eu/lv/c___{{lon}}-{{lat}}-{{zoom}}/bl___cl' },
        '_map_LGIA':   { save:1, title:'LĢIA Kartes',                    name:'LĢIA',       template:'https://kartes.lgia.gov.lv/?x={{lat}}&y={{lon}}&zoom={{zoom}}' },
        '_map_KDL':    { save:1, title:'Kadastrs',                       name:'Kadastrs',   template:'https://www.kadastrs.lv/map/di?xy={{lat}},{{lon}}&z={{zoom}}' },
        '_map_LVM':    { save:1, title:'LVM GEO',                        name:'LVM GEO',    template:'https://lvmgeo.lvm.lv/?loc={{lat}};{{lon}};{{zoom}}' },
        '_map_CFY':    { save:1, title:'Citify',                         name:'Citify',     template:'https://citify.eu/lv/?lng={{lon}}&lat={{lat}}&z={{zoom}}' },
        '_map_BIS':    { save:1, title:'BIS Plānotie būvdarbi',          name:'BIS',        template:'https://bis.gov.lv/bisp/lv/planned_constructions/bismap#x={{lon}}&y={{lat}}&z={{zoom}}' },
        '_map_LVC':    { save:1, title:'LVC Būvdarbi',                   name:'LVC',        template:'https://map.transportdata.gov.lv/public?mode=summer#{{zoom}}/{{lat}}/{{lon}}' },
        '_map_DODLV':  { save:1, title:'Dodies.lv',                      name:'Dodies',     template:'https://vesture.dodies.lv/#m={{zoom}}/{{lat}}/{{lon}}&l=J' },
        '_hdr_LT':     { type: 'header',                                 label:'Lithuania',   flag: 'LT' },
        '_map_MAPLT':  { save:1, title:'Maps.lt',                        name:'Maps.lt',    template:'https://maps.lt/map?c={{lon}}%2C{{lat}}&r=0&s={{zoom}}&b=topo&bl=false' },
        '_map_REGIA':  { save:1, title:'Regia.lt',                       name:'Regia',      template:'https://www.regia.lt/map/?x={{lon}}&y={{lat}}'},
    };

    // Keys excluded from the visible link bar and settings checkboxes
    const INTERNAL_KEYS = new Set(['_map_WME', '_map_LI']);

    // ============================================================================
    // LOCAL STORAGE HELPERS
    // ============================================================================

    function getLS(key, type, defaultVal) {
        const raw = localStorage.getItem(key);
        if (raw === null) return defaultVal;
        switch (type) {
            case 'bool': return raw === 'true' || raw === '1';
            case 'int':  return !isNaN(parseInt(raw)) ? parseInt(raw) : defaultVal;
            default:     return raw;
        }
    }

    function saveCheckboxStates() {
        const states = {};
        for (const id in Config) {
            if (!INTERNAL_KEYS.has(id)) states[id] = Config[id].save;
        }
        localStorage.setItem(LS.CHECKBOX_STATES, JSON.stringify(states));
    }

    function loadCheckboxStates() {
        const raw = localStorage.getItem(LS.CHECKBOX_STATES);
        if (!raw) return;
        const states = JSON.parse(raw);
        for (const id in states) {
            if (Config[id]) Config[id].save = states[id];
        }
    }

    // ============================================================================
    // SITE DETECTION
    // ============================================================================

    const HOSTNAME_MAP = {
        'www.waze.com':          'waze',
        'editor-beta.waze.com':  'waze',
        'beta.waze.com':         'waze',
        'www.kadastrs.lv':       'kdlv',
    };

    function getSiteType() {
        const h = location.hostname;
        if (HOSTNAME_MAP[h]) return HOSTNAME_MAP[h];
        if (h.startsWith('www.google.')) return 'google';
        return '';
    }

    // ============================================================================
    // URL / COORD HELPERS
    // ============================================================================

    function getQueryParam(url, name) {
        const prefix = name + '=';
        const idx = url.indexOf(prefix);
        if (idx < 0) return -1;
        const start = idx + prefix.length;
        const end   = url.indexOf('&', start);
        return end < 0 ? url.slice(start) : url.slice(start, end);
    }

    function getFaviconURL(id, template) {
        if (Config[id]?.icon) return Config[id].icon;
        const m = typeof template === 'string' && template.match(/https?:\/\/([^/]+)/);
        if (m) {
            const domain = m[1].replace(/^www\./, '').replace(/^map\./, '');
            return `https://icons.duckduckgo.com/ip3/${encodeURIComponent(domain)}.ico`;
        }
        return '';
    }

    // ============================================================================
    // GET LAT/LON/ZOOM FROM CURRENT PAGE
    // Replaces OpenLayers transforms with W.map SDK equivalents
    // ============================================================================

    function getWMECenter() {
        const center = W.map.getCenter();
        let lat = center.lat;
        let lon = center.lon;

        // If values look like Mercator (EPSG:900913) rather than WGS-84,
        // transform them. WGS-84 lat is always -90..90, lon -180..180.
        // Mercator values for anywhere on earth are in the millions.
        if (Math.abs(lat) > 90 || Math.abs(lon) > 180) {
            const ll = new OpenLayers.LonLat(lon, lat)
            .transform(
                new OpenLayers.Projection('EPSG:900913'),
                new OpenLayers.Projection('EPSG:4326')
            );
            lat = ll.lat;
            lon = ll.lon;
        }

        return { lat, lon, zoom: W.map.getZoom() };
    }

    function getLLZ() {
        const href     = location.href;
        const siteType = getSiteType();

        const EXTRACTORS = {
            waze() {
                return getWMECenter();
            },

            google() {
                const at = href.indexOf('@');
                if (at >= 0) {
                    const parts     = href.slice(at + 1).split(',');
                    const zoomParts = parts[2].replace(/([0-9]+)([zm]+)(\/.*)?\.?/, '$1.$2').split('.');
                    if (zoomParts[1] === 'm') {
                        const M2Z = [
                            {z:1,m:51510000},{z:2,m:25755000},{z:3,m:12877500},{z:4,m:6438750},{z:5,m:3219375},
                            {z:6,m:1609687},{z:7,m:804844},{z:8,m:402422},{z:9,m:201211},{z:10,m:100605},
                            {z:11,m:50303},{z:12,m:25151},{z:13,m:12576},{z:14,m:6288},{z:15,m:3144},
                            {z:16,m:1572},{z:17,m:786},{z:18,m:393},{z:19,m:196},{z:20,m:98},
                            {z:21,m:49},{z:22,m:25},{z:23,m:12},
                        ];
                        const z = parseInt(zoomParts[0]);
                        let zoom = 1;
                        for (let i = 0; i < M2Z.length - 1; i++) {
                            if (z <= M2Z[i].m && z >= M2Z[i + 1].m) { zoom = M2Z[i].z; break; }
                        }
                        return { lat: parts[0], lon: parts[1], zoom };
                    }
                    return { lat: parts[0], lon: parts[1], zoom: zoomParts[0] };
                }
                return {
                    lat:  getQueryParam(href, 'y'),
                    lon:  getQueryParam(href, 'x'),
                    zoom: parseInt(getQueryParam(href, 'z')),
                };
            },

            kdlv() {
                let frmap = null;
                for (let i = 0; i < frames.length; i++) {
                    if (typeof frames[i].esri !== 'undefined') { frmap = frames[i]; break; }
                }
                if (!frmap) return { lat: 0, lon: 0, zoom: 0 };
                frmap.document.getElementById('dijit_form_Button_15').click();
                const urlKdl = frmap.document.getElementById('dijit_Dialog_0').getElementsByTagName('textarea')[0].value;
                frmap.document.getElementsByClassName('dijitDialogCloseIcon')[0].click();
                const ll = getQueryParam(urlKdl, 'xy').split(',');
                return { lat: ll[1], lon: ll[0], zoom: parseInt(getQueryParam(urlKdl, 'z')) };
            },
        };

        if (siteType === 'mry' || siteType === 'sc') {
            return { lat: getQueryParam(href, 'lat'), lon: getQueryParam(href, 'lng'), zoom: parseInt(getQueryParam(href, 'z')) };
        }
        if (siteType === 'wm') {
            return { lat: getQueryParam(href, 'lat'), lon: getQueryParam(href, 'lon'), zoom: parseInt(getQueryParam(href, 'z')) };
        }

        const result = EXTRACTORS[siteType]?.() ?? { lat: 0, lon: 0, zoom: 0 };
        if (debug) console.log(`${SCRIPT_NAME}: getLLZ (${siteType}):`, result);
        return result;
    }

    // ============================================================================
    // COORDINATE TRANSFORMS — other sites → WME
    // ============================================================================

    function convertToWME(llz) {
        const siteType = getSiteType();

        const TRANSFORMS = {
            lgia() {
                const t  = proj4(proj4(LKS92_PROJ), proj4('EPSG:4326'), [parseFloat(llz.lon), parseFloat(llz.lat)]);
                llz.lon  = t[0]; llz.lat = t[1];
                llz.zoom = parseInt(llz.zoom, 10) + 7;
            },
            kdlv() {
                const t  = proj4(proj4(LKS92_PROJ), proj4('EPSG:4326'), [parseFloat(llz.lon), parseFloat(llz.lat)]);
                llz.lon  = t[0]; llz.lat = t[1];
                llz.zoom = 17;
            },
        };

        TRANSFORMS[siteType]?.();
        if (debug) console.log(`${SCRIPT_NAME}: convertToWME →`, llz);
        return llz;
    }

    // ============================================================================
    // COORDINATE TRANSFORMS — WME → external sites
    // ============================================================================

    function convertFromWME(id, llz) {
        if (id === '_map_LI') llz.zoom -= 1;

        const TRANSFORMS = {
            _map_Google() {
                if (location.href.includes('mapmaker')) llz.zoom++;
            },
            _map_BM() {
                // Project WGS-84 → EPSG:3857 for BalticMaps
                const rad = Math.PI / 180;
                llz.lon   = parseFloat(llz.lon) * 20037508.34 / 180;
                llz.lat   = Math.log(Math.tan((90 + parseFloat(llz.lat)) * rad / 2)) / rad * 20037508.34 / 180;
            },
            _map_LGIA() {
                const t  = proj4(proj4('EPSG:4326'), proj4(LKS92_PROJ), [parseFloat(llz.lon), parseFloat(llz.lat)]);
                llz.lon  = t[0]; llz.lat = t[1];
                llz.zoom = 9;
            },
            _map_KDL() {
                const t  = proj4(proj4('EPSG:4326'), proj4(LKS92_PROJ), [parseFloat(llz.lon), parseFloat(llz.lat)]);
                llz.lon  = t[1]; llz.lat = t[0];
                let z = Math.min(llz.zoom - 12, 7);
                for (const row of ARR_W2KDL) { if (z === row.w) { z = row.r; break; } }
                llz.zoom = z;
            },
            _map_LVM() {
                const t  = proj4(proj4('EPSG:4326'), proj4(LKS92_PROJ), [parseFloat(llz.lon), parseFloat(llz.lat)]);
                llz.lat  = t[0]; llz.lon = t[1];
                llz.zoom = 14;
            },
            _map_MRY() { llz.zoom--; },
            _map_WMFLAB() {
                const dms = (deg) => {
                    const d   = parseInt(deg);
                    const rem = deg - d;
                    const m   = parseInt(rem * 60);
                    const s   = Math.round((rem * 60 - m) * 60 * 10) / 10;
                    return `${d}_${m}_${s}`;
                };
                llz.lat  = dms(llz.lat);
                llz.lon  = dms(llz.lon);
                llz.zoom = Math.pow(2, 12 - llz.zoom) * 100000;
            },
            _map_OSV() { if (llz.zoom > 18) llz.zoom = 18; },
            _map_MAPLT() {
                const rad = Math.PI / 180;
                llz.lon  = parseFloat(llz.lon) * 20037508.34 / 180;
                llz.lat  = Math.log(Math.tan((90 + parseFloat(llz.lat)) * rad / 2)) / rad * 20037508.34 / 180;
                llz.zoom = 591657527.61 / Math.pow(2, llz.zoom);
            },
            _map_REGIA() {
                // Convert WGS-84 to LKS94 (EPSG:33406) for Regia.lt
                const t = proj4(proj4('EPSG:4326'), proj4(LKS94_PROJ), [parseFloat(llz.lon), parseFloat(llz.lat)]);
                llz.lon = Math.round(t[0]);  // Easting → x parameter
                llz.lat = Math.round(t[1] + 1200);  // Subtract 1200 meters to fix north offset

                // Convert WME zoom to Regia scale denominator
                const zoomMap = {
                    19: 750, 18: 1000, 17: 2000, 16: 3000,
                    15: 5000, 14: 10000, 13: 20000, 12: 50000
                };
                llz.zoom = zoomMap[llz.zoom] || 5000;
            },
        };

        TRANSFORMS[id]?.();
        if (debug) console.log(`${SCRIPT_NAME}: convertFromWME(${id}) →`, llz);
        return llz;
    }

    // ============================================================================
    // PERMALINK HELPER (for alpha/beta toggle)
    // ============================================================================

    function generatePermalink() {
        for (const a of document.querySelectorAll('.WazeControlPermalink a')) {
            const href = a.href;
            if (href.includes('.waze.com/') && href.includes('/editor')) {
                return href.replace('/ru/', '/').replace(/layers=([0-9]+)&/, '') + '&marker=yes';
            }
        }
        return '';
    }

    // ============================================================================
    // CLICK HANDLER — jump to map
    // ============================================================================

    function onLinkClick() {
        const id = this.id;
        if (debug) console.log(`${SCRIPT_NAME}: click → ${id}`);

        let savedSelection = [];
        if (restoreSelected && id !== '_map_WME' && !id.startsWith('_map_WME_')) {
            // SDK: W.selectionManager.getSelectedFeatures() is unchanged
            savedSelection = W.selectionManager.getSelectedFeatures().map(f => f.model);
        }

        let llz = getLLZ();

        if (id === '_map_WME' || id.startsWith('_map_WME_')) {
            llz = convertToWME(llz);
        } else {
            llz = convertFromWME(id, llz);
        }

        let template = Config[id]?.template ?? '';
        if (id.startsWith('_map_WME_')) template = Config['_map_WME'].template;

        const isWME = id === '_map_WME' || id.startsWith('_map_WME_');
        let url = template
        .replace('{{lon}}',  llz.lon)
        .replace('{{lat}}',  llz.lat)
        .replace('{{zoom}}', llz.zoom)
        + (isWME ? '&marker=yes' : '');

        if (savedSelection.length > 0) {
            setTimeout(() => W.selectionManager.select(savedSelection), 50);
        }

        if (id.startsWith('_map_WME_')) {
            const jmlink = this.getAttribute('jmlink');
            if (this.getAttribute('jmfrom') === 'mapbys') {
                window.open(`http://map.nca.by/map.html?xy=${jmlink}&z=16`, `_url_jm${id}`);
            }
            url = url.split('&')[0] + `&jmlink=${jmlink}`;
        }

        window.open(url, `_url${id}`);
    }

    // ============================================================================
    // WME FLOATING LINK BAR
    // ============================================================================

    function buildLinkBar() {
        let nod = document.getElementById(PANEL_ID);
        if (nod) {
            nod.innerHTML = '';
        } else {
            nod = document.createElement('div');
            nod.id = PANEL_ID;
            nod.setAttribute('unselectable', 'on');
            nod.style.cssText = `font-size:12px; color:#fff; padding-left:20px; position:absolute;
                top:${topOffset}; left:${leftOffset}; display:block;
                background-color:rgba(0,0,0,.7);
                visibility:${hideWindow ? 'hidden' : 'visible'}; cursor:pointer;`;
        }

        loadCheckboxStates();

        let html = '';
        for (const id in Config) {
            if (INTERNAL_KEYS.has(id) || Config[id].save !== 1) continue;
            const { title, name, template } = Config[id];
            const icon    = getFaviconURL(id, template);
            const iconTag = icon ? `<img src="${icon}" style="width:12px;height:12px;vertical-align:middle;margin-right:4px;">` : '';
            html += `<a id="${id}" style="color:#fff;font-size:11px" title="${title}">${iconTag}${name}</a>&nbsp;<span style="opacity:0.4;">|</span>&nbsp;`;
        }

        const isMain = location.hostname === 'www.waze.com';
        html +=
            `<a id="_map_LI" style="color:#fff;font-size:10px" title="Open in LiveMap">[Live]</a>&nbsp;` +
            `<a id="_map_AB" tp="${isMain ? 'A' : 'B'}" href="" style="color:#fff;font-size:10px"` +
            ` title="Open in ${isMain ? 'Beta' : 'Main'} editor"` +
            ` target="WME-JumpMaps-${isMain ? 'b' : 'a'}">[${isMain ? '&#946;' : '&#945;'}]</a>&nbsp;` +
            `<a href="https://greasyfork.org/en/scripts/481079-wme-jumpmaps"` +
            ` title="WME-JumpMaps_${SCRIPT_VERSION}" style="color:#fff;font-size:10px" target="_blank">[?]</a>&nbsp;`;

        nod.innerHTML = html;
        document.getElementById('waze-map-container').parentElement.appendChild(nod);

        for (const id in Config) {
            document.getElementById(id)?.addEventListener('click', onLinkClick);
        }
        document.getElementById('_map_LI')?.addEventListener('click', onLinkClick);
        document.getElementById('_map_AB')?.addEventListener('click', function () {
            const permalink = '?' + generatePermalink().split('?')[1];
            const toMain    = this.getAttribute('tp') === 'A';
            this.href = (toMain ? 'https://beta.waze.com/editor' : 'https://www.waze.com/editor') + permalink;
        });

        if (!fixedPos) makeDraggable(nod, nod, () => {
            topOffset  = nod.style.top;
            leftOffset = nod.style.left;
            localStorage.setItem(LS.TOP_OFFSET,  topOffset);
            localStorage.setItem(LS.LEFT_OFFSET, leftOffset);
        });
    }

    // ============================================================================
    // ICON INSERTION — external (non-WME) sites
    // ============================================================================

    function insertExternalIcon() {
        const siteType = getSiteType();
        if (siteType === 'waze') return true;

        const INSERTION = {
            kdlv: {
                anchor: () => document.getElementById('social_networks'),
                insert(anchor) {
                    anchor.insertAdjacentHTML('afterbegin', '<a id="_map_WME" class="waze" target="_blank" title="Open in WME"></a>');
                    document.styleSheets[0].insertRule(`#social_networks a.waze { background:url(${ICON_WME});background-size:100% 100%;right:356px}`, 0);
                },
            },
        };

        const cfg = INSERTION[siteType];
        if (!cfg) return true;

        const anchor = cfg.anchor();
        if (!anchor) {
            setTimeout(insertExternalIcon, 500);
            return false;
        }

        let nod = document.getElementById(PANEL_ID);
        if (!nod) {
            nod    = document.createElement('span');
            nod.id = PANEL_ID;
        }
        cfg.insert(anchor, nod);

        document.getElementById('_map_WME')?.addEventListener('click', onLinkClick);
        return true;
    }

    // ============================================================================
    // MINIMAP
    // Uses W.map SDK events instead of OpenLayers event registration
    // ============================================================================

    function createMinimap() {
        if (minimapInstance) return;

        if (!document.getElementById('leaflet-minimap-css')) {
            const link = document.createElement('link');
            link.id    = 'leaflet-minimap-css';
            link.rel   = 'stylesheet';
            link.href  = 'https://unpkg.com/[email protected]/dist/leaflet.css';
            document.head.appendChild(link);
        }

        const mmTop  = localStorage.getItem(LS.MINIMAP_TOP);
        const mmLeft = localStorage.getItem(LS.MINIMAP_LEFT) ?? DEFAULT_MINIMAP_LEFT;
        const container = document.createElement('div');
        container.id    = 'latvia-minimap';
        // Restore saved position; fall back to bottom:30px if no saved top
        if (mmTop) {
            container.style.cssText = `position:absolute;top:${mmTop};left:${mmLeft};width:220px;height:145px;z-index:1;border:1px solid #333;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,.3);background:white;overflow:hidden;`;
        } else {
            container.style.cssText = `position:absolute;bottom:30px;left:${mmLeft};width:220px;height:145px;z-index:1;border:1px solid #333;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,.3);background:white;overflow:hidden;`;
        }

        const header = document.createElement('div');
        header.style.cssText = 'background:#007acc;color:white;font-weight:400;height:20px;font-size:13px;text-align:center;cursor:move;user-select:none;font-family:Rubik;';

        const updateHeader = () => {
            const el = document.querySelector('.location-info');
            if (el?.textContent) header.textContent = el.textContent;
        };
        setTimeout(updateHeader, 1000);
        setInterval(updateHeader, 2000);
        container.appendChild(header);

        const mapDiv = document.createElement('div');
        mapDiv.id    = 'minimap-leaflet';
        mapDiv.style.cssText = 'width:100%;height:calc(100% - 20px);';
        container.appendChild(mapDiv);

        document.querySelector('#map').appendChild(container);
        makeDraggable(container, header, () => {
            localStorage.setItem(LS.MINIMAP_TOP,  container.style.top);
            localStorage.setItem(LS.MINIMAP_LEFT, container.style.left);
        });
        minimapInstance = container;

        setTimeout(() => {
            const minimap = L.map('minimap-leaflet', {
                zoomControl: false, attributionControl: false,
                dragging: false, scrollWheelZoom: false,
                doubleClickZoom: false, boxZoom: false, keyboard: false, touchZoom: false,
            });

            L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 18 }).addTo(minimap);

            const COUNTRY_BOUNDS = {
                LV: [[55.67, 20.97], [58.09, 28.24]],
                LT: [[53.86, 21.05], [56.39, 25.97]],
                EE: [[57.51, 21.76], [59.68, 28.21]],
            };

            function detectCountryFromUI() {
                const el = document.querySelector('.location-info');
                if (!el || !el.textContent) return 'LV'; // fallback

                const txt = el.textContent.toLowerCase();

                if (txt.includes('lithuania') || txt.includes('lietuva')) return 'LT';
                if (txt.includes('estonia') || txt.includes('eesti')) return 'EE';
                if (txt.includes('latvia') || txt.includes('latvija')) return 'LV';

                return 'LV'; // safe fallback
            }

            function applyBounds() {
                const country = detectCountryFromUI();
                const bounds = COUNTRY_BOUNDS[country] || COUNTRY_BOUNDS.LV;
                minimap.fitBounds(bounds);
            }

            // initial fit (delay ensures location-info is populated)
            setTimeout(applyBounds, 800);

            // optional: keep it updated if you move across borders
            setInterval(applyBounds, 3000);

            const marker = L.circleMarker([56.95, 24.1], {
                radius: 5, color: '#ffffff', fillColor: '#0099ff', fillOpacity: 0.8, weight: 1,
            }).addTo(minimap);

            function updateMarker() {
                try {
                    const c = W.map?.getCenter();
                    if (!c) return;
                    // SDK: use W.map.getCenter() — returns EPSG:900913, project to WGS-84
                    const ll = new OpenLayers.LonLat(c.lon, c.lat)
                    .transform(
                        new OpenLayers.Projection('EPSG:900913'),
                        new OpenLayers.Projection('EPSG:4326')
                    );
                    marker.setLatLng([ll.lat, ll.lon]);
                } catch (e) {
                    console.error(`${SCRIPT_NAME}: minimap update error`, e);
                }
            }

            // SDK: subscribe to map move/zoom via W.map.events (OL2 still present in WME SDK)
            W.map.events.register('moveend', null, updateMarker);
            W.map.events.register('zoomend', null, updateMarker);
            updateMarker();

            minimap.on('click', ({ latlng }) => {
                // SDK: W.map.setCenter accepts an OL LonLat in EPSG:900913
                const pt = new OpenLayers.LonLat(latlng.lng, latlng.lat)
                .transform(
                    new OpenLayers.Projection('EPSG:4326'),
                    new OpenLayers.Projection('EPSG:900913')
                );
                W.map.setCenter(pt);
            });
        }, 500);
    }

    function destroyMinimap() {
        minimapInstance?.remove();
        minimapInstance = null;
    }

    // ============================================================================
    // DRAGGABLE HELPER
    // ============================================================================

    function makeDraggable(element, handle, onDragEnd) {
        handle = handle ?? element;
        let x0 = 0, y0 = 0, x1 = 0, y1 = 0;

        function getMapRect() {
            return document.getElementById('waze-map-container')?.getBoundingClientRect()
                ?? document.body.getBoundingClientRect();
        }

        handle.onmousedown = (e) => {
            e.preventDefault();
            x1 = e.clientX; y1 = e.clientY;

            document.onmouseup = () => {
                document.onmouseup = document.onmousemove = null;
                if (onDragEnd) onDragEnd();
            };

            document.onmousemove = (e) => {
                e.preventDefault();
                x0 = x1 - e.clientX; y0 = y1 - e.clientY;
                x1 = e.clientX;      y1 = e.clientY;

                const mr     = getMapRect();
                const parent = element.offsetParent?.getBoundingClientRect() ?? mr;

                // New position in parent-relative coords
                let newTop  = element.offsetTop  - y0;
                let newLeft = element.offsetLeft - x0;

                // Clamp within map bounds (converted to parent-relative)
                const mapTop    = mr.top  - parent.top;
                const mapLeft   = mr.left - parent.left;
                const mapRight  = mapLeft + mr.width  - element.offsetWidth;
                const mapBottom = mapTop  + mr.height - element.offsetHeight;

                newTop  = Math.max(mapTop,  Math.min(newTop,  mapBottom));
                newLeft = Math.max(mapLeft, Math.min(newLeft, mapRight));

                element.style.top    = newTop  + 'px';
                element.style.left   = newLeft + 'px';
                element.style.bottom = element.style.right = 'auto';
            };
        };

        // Also keep within bounds on window resize
        window.addEventListener('resize', () => {
            if (element.style.top === '' || element.style.left === '') return;
            const mr     = getMapRect();
            const parent = element.offsetParent?.getBoundingClientRect() ?? mr;
            const mapTop    = mr.top  - parent.top;
            const mapLeft   = mr.left - parent.left;
            const mapRight  = mapLeft + mr.width  - element.offsetWidth;
            const mapBottom = mapTop  + mr.height - element.offsetHeight;
            element.style.top  = Math.max(mapTop,  Math.min(parseFloat(element.style.top),  mapBottom)) + 'px';
            element.style.left = Math.max(mapLeft, Math.min(parseFloat(element.style.left), mapRight))  + 'px';
        });
    }

    // ============================================================================
    // SETTINGS PANEL
    // Replaces WazeWrap.Interface.Tab() with the native WME SDK tab API
    // ============================================================================

    /**
     * Registers a tab in the WME side panel.
     *
     * onReadyCallback receives the root container element as its first argument
     * so all DOM queries can be scoped to it — critical for the modern SDK where
     * the tab pane may live inside a shadow DOM or disconnected subtree.
     */
    function registerSDKTab(tabLabel, contentHTML, onReadyCallback) {
        // ── Path 1: Modern SDK (W.userscripts, available ~2024+) ─────────────
        if (typeof W.userscripts?.registerSidebarTab === 'function') {
            const { tabLabel: label, tabPane } = W.userscripts.registerSidebarTab(tabLabel);
            label.innerText   = tabLabel;
            tabPane.innerHTML = contentHTML;

            // Wire up handlers every time the tab becomes visible
            tabPane.addEventListener('wme-tab-pane-activate', () => onReadyCallback(tabPane));

            // Also wire immediately — the pane is in the DOM from this point
            onReadyCallback(tabPane);
            return;
        }

        // ── Path 2: Older SDK — inject tab manually into the WME sidebar ─────
        const navTabs  = document.querySelector('#sidebar .nav-tabs');
        const tabPanes = document.querySelector('#sidebar .tab-content');
        if (!navTabs || !tabPanes) {
            console.warn(`${SCRIPT_NAME}: sidebar not found, retrying tab registration…`);
            setTimeout(() => registerSDKTab(tabLabel, contentHTML, onReadyCallback), 1000);
            return;
        }

        const tabId = `wme-jm-tab-${tabLabel.toLowerCase().replace(/\s+/g, '-')}`;
        navTabs.insertAdjacentHTML('beforeend',
                                   `<li><a data-toggle="tab" href="#${tabId}">${tabLabel}</a></li>`);
        tabPanes.insertAdjacentHTML('beforeend',
                                    `<div class="tab-pane" id="${tabId}">${contentHTML}</div>`);

        const pane = document.getElementById(tabId);

        // Re-wire every time the tab is clicked (handles dynamic re-renders)
        navTabs.querySelector(`a[href="#${tabId}"]`)
            ?.addEventListener('click', () => onReadyCallback(pane));

        // Wire once immediately
        onReadyCallback(pane);
    }

    /**
     * Wire up all interactive settings controls.
     * @param {Element} root  — the tab pane container; ALL queries are scoped here.
     */
    function onSettingsReady(root) {
        // Guard: if the expected content isn't present yet, bail silently
        if (!root || !root.querySelector('[data-id]')) return;

        const $ = (sel) => root.querySelector(sel);
        const $$ = (sel) => root.querySelectorAll(sel);

        // ── Map service checkboxes ────────────────────────────────────────────
        for (const chk of $$('input[type="checkbox"][data-id]')) {
            const id = chk.dataset.id;
            if (!Config[id]) continue;
            chk.checked = !!Config[id].save;
            chk.onchange = function () {
                Config[this.dataset.id].save = this.checked ? 1 : 0;
                saveCheckboxStates();
                buildLinkBar();
            };
        }

        // ── Debug ─────────────────────────────────────────────────────────────
        const dbgChk = $('#wmejm_cfg_debug');
        if (dbgChk) {
            dbgChk.checked = debug;
            dbgChk.onchange = function () {
                debug = this.checked;
                localStorage.setItem(LS.DEBUG, debug ? '1' : '0');
            };
        }

        // ── Hide window ───────────────────────────────────────────────────────
        const hideChk = $('#wmejm_cfg_window_hide');
        if (hideChk) {
            hideChk.checked = hideWindow;
            hideChk.onchange = function () {
                hideWindow = this.checked;
                localStorage.setItem(LS.HIDE_WINDOW, hideWindow ? '1' : '0');
                document.getElementById(PANEL_ID).style.visibility = hideWindow ? 'hidden' : 'visible';
            };
        }

        // ── Fixed position — locks bar in place (disables dragging only) ────────
        const fixedChk = $('#wmejm_cfg_fixed_pos');
        if (fixedChk) {
            fixedChk.checked = fixedPos;
            fixedChk.onchange = function () {
                fixedPos = this.checked;
                localStorage.setItem(LS.FIXED_POS, fixedPos ? '1' : '0');
                const bar = document.getElementById(PANEL_ID);
                if (!bar) return;
                if (fixedPos) {
                    bar.onmousedown = null;   // disable drag
                } else {
                    makeDraggable(bar, bar, () => {
                        topOffset  = bar.style.top;
                        leftOffset = bar.style.left;
                        localStorage.setItem(LS.TOP_OFFSET,  topOffset);
                        localStorage.setItem(LS.LEFT_OFFSET, leftOffset);
                    }); // re-enable drag
                }
            };
        }

        // ── Reset window position ─────────────────────────────────────────────
        $('#wmejm_cfg_resetWPos')?.addEventListener('click', () => {
            localStorage.removeItem(LS.MINIMAP_TOP);
            localStorage.removeItem(LS.MINIMAP_LEFT);
            const bar = document.getElementById(PANEL_ID);
            if (bar) {
                bar.style.top  = topOffset  = DEFAULT_TOP;
                bar.style.left = leftOffset = DEFAULT_LEFT;
                localStorage.setItem(LS.TOP_OFFSET,  DEFAULT_TOP);
                localStorage.setItem(LS.LEFT_OFFSET, DEFAULT_LEFT);
            }
            // Also reset minimap to default position
            if (minimapInstance) {
                minimapInstance.style.top    = '';
                minimapInstance.style.bottom = '30px';
                minimapInstance.style.left   = DEFAULT_MINIMAP_LEFT;
            }
        });

        // ── Reset config ──────────────────────────────────────────────────────
        $('#wmejm_cfg_resetConfig')?.addEventListener('click', () => {
            setTimeout(() => {
                if (!confirm('Reset config for WME-JumpMaps?')) return;
                localStorage.removeItem(LS.CHECKBOX_STATES);
                localStorage.removeItem(LS.DEBUG);
                localStorage.removeItem(LS.MINIMAP_TOP);
                localStorage.removeItem(LS.MINIMAP_LEFT);
                debug = false;
                Config = JSON.parse(JSON.stringify(CONFIG_DEFAULTS));
                // Reposition minimap to default if it's visible
                if (minimapInstance) {
                    minimapInstance.style.top    = '';
                    minimapInstance.style.bottom = '30px';
                    minimapInstance.style.left   = DEFAULT_MINIMAP_LEFT;
                }
                buildLinkBar();
                buildSettingsPanel();
            }, 100);
        });

        // ── Minimap ───────────────────────────────────────────────────────────
        const mapChk = $('#wmejm_cfg_minimap');
        if (mapChk) {
            mapChk.checked = getLS(LS.SHOW_MINIMAP, 'bool', true);
            mapChk.onchange = function () {
                showMinimap = this.checked;
                localStorage.setItem(LS.SHOW_MINIMAP, showMinimap ? '1' : '0');
                showMinimap ? createMinimap() : destroyMinimap();
            };
            if (mapChk.checked) setTimeout(createMinimap, 1000);
        }
    }

    let _settingsPanelRegistered = false;

    function buildSettingsPanel() {
        const tabVersionId = `WME-JumpMaps-${SCRIPT_VERSION.replace(/\./g, '-')}`;
        // Use a module-level flag — the tabVersionId div may live outside document scope
        // in the modern SDK, so getElementById would silently fail as a guard.
        if (_settingsPanelRegistered) return;
        _settingsPanelRegistered = true;

        loadCheckboxStates();

        let rows = '';
        for (const id in Config) {
            if (INTERNAL_KEYS.has(id)) continue;

            const item = Config[id];

            // --- HEADER (no checkbox) ---
            if (item.type === 'header') {
                const flag = item.flag
                ? `<img src="https://flagcdn.com/w20/${item.flag.toLowerCase()}.png" style="width:16px;height:12px;">`
            : '';

        rows += `
            <div style="margin:10px 0 4px 0; padding-left:20px;">
                <div style="display:flex;align-items:center;gap:6px;font-weight:600;font-size:13px;">
                    ${flag}
                    <span>${item.label}</span>
                </div>
            </div>`;
        continue;
    }

    // --- NORMAL ITEM ---
    const { title, save, template } = item;

    const baseURL = template?.match(/https?:\/\/[^/]+/)?.[0] ?? '';
    const fav     = getFaviconURL(id, template);

    // prefer flag if present, fallback to favicon
    const iconTag = item.flag
    ? `<img src="https://flagcdn.com/w20/${item.flag.toLowerCase()}.png" style="width:16px;height:12px;">`
        : (fav ? `<img src="${fav}" style="width:16px;height:16px;">` : '');

    rows += `
        <div class="form-group" style="margin-bottom:4px;">
            <div class="control-label" style="display:flex;align-items:center;gap:6px;font-weight:normal;font-size:14px;line-height:16px;">
                <input data-id="${id}" name="wmejm_cfg_${id}" id="wmejm_cfg_${id}" type="checkbox" ${save ? 'checked' : ''}>
                ${iconTag}
                <label id="wmejm_cfg_${id}_chklab" for="wmejm_cfg_${id}" style="cursor:pointer;font-weight:normal;margin:0;" title="${baseURL}">
                    ${title}
                </label>
            </div>
        </div>`;
}
        const html = `
            <div id="${tabVersionId}" class="side-panel-section">
                <h4>${SCRIPT_NAME}</h4>
                <div class="attributes-form side-panel-section">
                    <div class="form-group">
                        <label class="control-label">Map services:</label>
                        <div class="controls">${rows}</div>
                    </div>
                    <div class="form-group">
                        <label class="control-label">Other settings:</label>
                        <div class="controls" style="margin:0;line-height:16px;">
                            <input type="checkbox" id="wmejm_cfg_minimap">
                            <label for="wmejm_cfg_minimap" style="cursor:pointer;margin-left:6px;font-weight:normal;margin-bottom:0">
                                <i class="fa fa-map"></i> Show minimap
                            </label><br>
                            <input type="checkbox" id="wmejm_cfg_fixed_pos">
                            <label for="wmejm_cfg_fixed_pos" style="cursor:pointer;margin-left:6px;font-weight:normal;">
                                <i class="fa fa-lock"></i> Fixed link bar position
                            </label><br>
                            <input type="checkbox" id="wmejm_cfg_window_hide">
                            <label for="wmejm_cfg_window_hide" style="cursor:pointer;margin-left:6px;font-weight:normal;">
                                <i class="fa fa-eye-slash"></i> Hide link bar
                            </label><br>
                            <input type="checkbox" id="wmejm_cfg_debug">
                            <label for="wmejm_cfg_debug" style="cursor:pointer;margin-left:6px;font-weight:normal;margin-bottom:0">
                                <i class="fa fa-bug"></i> Debug script (console logging)
                            </label><br>
                            <button id="wmejm_cfg_resetConfig" class="btn btn-default" style="font-size:9px;padding:5px 9px;margin:4px 0;">
                                <i class="fa fa-recycle"></i> Reset config
                            </button>
                            <button id="wmejm_cfg_resetWPos" class="btn btn-default" style="font-size:9px;padding:5px 9px;">
                                <i class="fa fa-recycle"></i> Reset window position
                            </button>
                        </div>
                    </div>
                </div>
            </div>`;

        registerSDKTab('JumpMaps', html, onSettingsReady);
    }

    // ============================================================================
    // HANDLE jmlink PARAM ON WME LOAD
    // ============================================================================

    function handleJMLink() {
        const jmlink = getQueryParam(location.href, 'jmlink');
        if (jmlink === -1) return;
        const parts = jmlink.split(',');
        // Project the stored point (EPSG:4326) back to EPSG:900913 for W.map.setCenter
        const pt = new OpenLayers.LonLat(parts[1], parts[0])
        .transform(
            new OpenLayers.Projection('EPSG:4326'),
            new OpenLayers.Projection('EPSG:900913')
        );
        W.map.setCenter(pt);
    }

    // ============================================================================
    // BOOTSTRAP
    // Replaces WazeWrap.Ready with the WME SDK event system
    // ============================================================================

    /**
     * Waits for the WME SDK to be fully ready.
     *
     * The SDK dispatches a 'wme-ready' CustomEvent on document when the map
     * and all core models are loaded.  We also poll as a safety net for
     * environments where the event may have already fired before our script ran.
     */
    function waitForWMEReady(callback) {
        // Already ready?
        if (W?.map && W?.selectionManager && document.getElementById('waze-map-container')) {
            callback();
            return;
        }

        // Listen for the SDK-dispatched ready event
        document.addEventListener('wme-ready', function onReady() {
            document.removeEventListener('wme-ready', onReady);
            callback();
        });

        // Polling fallback (covers cases where wme-ready fired before injection)
        let polls = 0;
        const poll = setInterval(() => {
            if (W?.map && W?.selectionManager && document.getElementById('waze-map-container')) {
                clearInterval(poll);
                callback();
            } else if (++polls > 120) {
                clearInterval(poll);
                console.error(`${SCRIPT_NAME}: timed out waiting for WME to be ready`);
            }
        }, 500);
    }

    function init() {
        const siteType = getSiteType();

        if (document.getElementById(PANEL_ID)) return; // already initialised

        if (siteType === 'waze') {
            handleJMLink();
            setTimeout(() => {
                buildLinkBar();
                buildSettingsPanel();
            }, 1000);
            return;
        }

        // External sites
        if (!insertExternalIcon()) {
            if (++initProbeCount < 8) setTimeout(init, 5000);
        }
    }

    function bootstrap() {
        console.log(`${SCRIPT_NAME} (${SCRIPT_VERSION}): bootstrap`);
        Config = JSON.parse(JSON.stringify(CONFIG_DEFAULTS));

        debug           = getLS(LS.DEBUG,       'bool', false);
        restoreSelected = getLS(LS.RESTORE_SEL, 'bool', false);
        hideWindow      = getLS(LS.HIDE_WINDOW, 'bool', false);
        fixedPos        = getLS(LS.FIXED_POS,   'bool', true);
        topOffset       = getLS(LS.TOP_OFFSET,  'string', DEFAULT_TOP);
        leftOffset      = getLS(LS.LEFT_OFFSET, 'string', DEFAULT_LEFT);

        loadCheckboxStates();

        const siteType = getSiteType();
        if (siteType === 'waze') {
            // Use SDK readiness detection instead of WazeWrap.Ready
            waitForWMEReady(init);
        } else {
            // External sites have no WME SDK; init directly after a short delay
            setTimeout(init, 500);
        }
    }

    bootstrap();
})();