WME Map Overlay (Optimized)

Sobreposição de mapas no WME: Google, OSM, Trânsito, Apple Maps, Waze Live Map, Mapillary e ViaMichelin! Só baixa tiles em segundo plano quando os sliders estão visíveis.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         WME Map Overlay (Optimized)
// @namespace    https://greasyfork.org/en/users/1559074-xtryker
// @version      5.0
// @description  Sobreposição de mapas no WME: Google, OSM, Trânsito, Apple Maps, Waze Live Map, Mapillary e ViaMichelin! Só baixa tiles em segundo plano quando os sliders estão visíveis.
// @author       Xtryker e Jabc82
// @match        https://www.waze.com/*editor*
// @grant        none
// @require      https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    let trafficDiv, sliderContainer, isSliderVisible = false;
    let wazeLiveLayer, gmap, mapillaryDiv, mapillaryIframe, trafficLayerRef;

    // Keep track of each layer's current slider value so we know, when the
    // panel is re-opened, which layers should actually resume fetching tiles.
    const sliderValues = {
        "Waze Live Map": 0,
        "Google Maps": 0,
        "Traffic": 0,
        "OSM": 0,
        "Mapillary": 0,
        "Apple Maps": 0,
        "ViaMichelin": 0
    };

    // All OpenLayers XYZ layers we manage (populated in initOverlay)
    let tileLayers = {};

    function initOverlay() {
        if (typeof W === 'undefined' || typeof W.map === 'undefined') {
            setTimeout(initOverlay, 1000);
            return;
        }

        const map = W.map;

        // NOTE: visibility starts as false for every layer. In OpenLayers,
        // opacity is purely a rendering/CSS effect — a layer with opacity 0
        // but visibility true will still issue tile requests in the
        // background. Setting visibility:false is what actually stops the
        // tile loader from firing requests, so we use it as our "should this
        // layer be downloading right now" switch.
        const googleBaseLayer = new OpenLayers.Layer.XYZ(
            "Google Maps (Base)",
            "https://mt.google.com/vt/lyrs=m&x=${x}&y=${y}&z=${z}",
            { isBaseLayer: false, opacity: 0.00, visibility: false }
        );

        const osmLayer = new OpenLayers.Layer.XYZ(
            "OpenStreetMap",
            "https://tile.openstreetmap.org/${z}/${x}/${y}.png",
            { isBaseLayer: false, opacity: 0.00, visibility: false }
        );

        const appleLayer = new OpenLayers.Layer.XYZ(
            "Apple Maps",
            "https://maps.apple.com/frame?map=explore&center=${lat},${lon}&span=${spanX},${spanY}",
            { isBaseLayer: false, opacity: 0.00, visibility: false }
        );

        wazeLiveLayer = new OpenLayers.Layer.XYZ(
            "Waze Live Map",
            "https://worldtiles1.waze.com/tiles/${z}/${x}/${y}.png",
            { isBaseLayer: false, opacity: 0.00, visibility: false }
        );

        // NOTE: ViaMichelin has no public/official tile API (their real API
        // is a paid B2B contract product - see api.viamichelin.com). This
        // URL is a reverse-engineered internal endpoint their own web
        // client calls, same category of risk as the Google layer above:
        // undocumented, unauthenticated, and can change or get blocked
        // without notice. The `version` query param is pinned to a build
        // timestamp from ViaMichelin's client; if tiles start coming back
        // empty/404, that's the first thing to suspect and update.
        // Three round-robin subdomains (map1/2/3), matching how their own
        // client spreads requests.
        const viaMichelinLayer = new OpenLayers.Layer.XYZ(
            "ViaMichelin",
            [
                "https://map1.viamichelin.com/map/mapdirect?map=viamichelin&z=${z}&x=${x}&y=${y}&version=201503191157&format=png&layer=background&locale=default",
                "https://map2.viamichelin.com/map/mapdirect?map=viamichelin&z=${z}&x=${x}&y=${y}&version=201503191157&format=png&layer=background&locale=default",
                "https://map3.viamichelin.com/map/mapdirect?map=viamichelin&z=${z}&x=${x}&y=${y}&version=201503191157&format=png&layer=background&locale=default"
            ],
            { isBaseLayer: false, opacity: 0.00, visibility: false }
        );

        map.addLayer(wazeLiveLayer);
        map.addLayer(googleBaseLayer);
        map.addLayer(osmLayer);
        map.addLayer(appleLayer);
        map.addLayer(viaMichelinLayer);

        tileLayers = {
            "Waze Live Map": wazeLiveLayer,
            "Google Maps": googleBaseLayer,
            "OSM": osmLayer,
            "Apple Maps": appleLayer,
            "ViaMichelin": viaMichelinLayer
            // "Traffic" is added once initTrafficLayer() runs, below.
        };

        sliderContainer = document.createElement("div");
        sliderContainer.style.position = "absolute";
        sliderContainer.style.top = "80px";
        sliderContainer.style.left = "50%";
        sliderContainer.style.transform = "translateX(-50%)";
        sliderContainer.style.zIndex = "1000";
        sliderContainer.style.padding = "8px";
        sliderContainer.style.background = "rgba(10, 25, 50, 0.95)";
        sliderContainer.style.borderRadius = "10px";
        sliderContainer.style.border = "1px solid white";
        sliderContainer.style.boxShadow = "0 2px 6px rgba(0,0,0,0.3)";
        sliderContainer.style.display = "none";
        sliderContainer.style.flexDirection = "row";
        sliderContainer.style.gap = "10px";
        sliderContainer.style.fontFamily = "sans-serif";
        sliderContainer.style.transition = "all 0.3s ease";

        const layers = [
            {
                name: "Waze Live Map",
                icon: "https://cdn-images-1.medium.com/max/1200/1*3kS1iOOTBrvtkecae3u2aA.png",
                initial: 0.00,
                onChange: value => wazeLiveLayer.setOpacity(value)
            },
            {
                name: "Google Maps",
                icon: "https://static.vecteezy.com/system/resources/previews/016/716/478/non_2x/google-maps-icon-free-png.png",
                initial: 0.00,
                onChange: value => googleBaseLayer.setOpacity(value)
            },
            {
                name: "Traffic",
                icon: "https://p7.hiclipart.com/preview/798/15/811/logo-font-traffic-jam-thumbnail.jpg",
                initial: 0.00,
                onChange: value => {
                    if (trafficLayerRef) trafficLayerRef.setOpacity(value);
                }
            },
            {
                name: "OSM",
                icon: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQPC9m4tfa2uII2-_l3png4g2jIF1yJw8p1uXweLk7pYw&s=10",
                initial: 0.00,
                onChange: value => osmLayer.setOpacity(value)
            },
            {
                name: "ViaMichelin",
                icon: "https://i.imgur.com/Kyek7rE.png",
                initial: 0.00,
                onChange: value => tileLayers["ViaMichelin"].setOpacity(value)
            },
            {
                name: "Mapillary",
                icon: "https://pbs.twimg.com/profile_images/1097399669158825984/aXZ49j3I_400x400.png",
                initial: 0.00,
                onChange: value => {
                    if (mapillaryDiv) mapillaryDiv.style.opacity = value;
                }
            },
            {
                name: "Apple Maps",
                icon: "https://th.bing.com/th/id/R.7bbe9deedfa162a68a224e18fbad2dfc?rik=nsbux2xtDIgyUw&pid=ImgRaw&r=0",
                initial: 0.00,
                onChange: value => appleLayer.setOpacity(value)
            },
            {
                name: "Websig PT",
                icon: "https://cdn-icons-png.flaticon.com/512/5379/5379902.png",
                initial: 0.00,
                onChange: () => {},
                isButton: true
            }
        ];

        layers.forEach(layer => {
            const wrapper = document.createElement("div");
            wrapper.style.width = "60px";
            wrapper.style.textAlign = "center";

            const img = document.createElement("img");
            img.src = layer.icon;
            img.alt = layer.name;
            img.title = layer.name;
            img.style.width = "60px";
            img.style.height = "60px";
            img.style.borderRadius = "12px";
            img.style.border = "2px solid white";
            img.style.transition = "transform 0.3s ease, border 0.3s ease";
            img.style.display = "block";
            img.style.marginBottom = "4px";
            img.style.cursor = "pointer";

            img.addEventListener("mouseenter", () => {
                img.style.transform = "scale(1.1)";
                img.style.border = "2px solid gold";
            });

            img.addEventListener("mouseleave", () => {
                img.style.transform = "scale(1)";
                img.style.border = "2px solid white";
            });

            img.addEventListener("click", () => {
                const center = W.map.getCenter();
                const zoom = Math.round(W.map.getZoom() - 3);
                const lonlat = new OpenLayers.LonLat(center.lon, center.lat);
                lonlat.transform(
                    new OpenLayers.Projection("EPSG:900913"),
                    new OpenLayers.Projection("EPSG:4326")
                );
                const lat = parseFloat(lonlat.lat.toFixed(6));
                const lon = parseFloat(lonlat.lon.toFixed(6));
                const span = 360 / Math.pow(2, zoom);

                if (layer.name === "Google Maps")
                    window.open(`https://www.google.com/maps/@${lat},${lon},${zoom + 3}z`, '_blank');
                if (layer.name === "OSM")
                    window.open(`https://www.openstreetmap.org/#map=${zoom + 3}/${lat}/${lon}`, '_blank');
                if (layer.name === "Websig PT")
                    window.open(`https://www.arcgis.com/apps/mapviewer/index.html?webmap=8c00d5d66ee24a0d94a7e923ea8f653e&center=${lon},${lat}&level=${zoom + 3}`, '_blank');
                if (layer.name === "Apple Maps")
                    window.open(`https://maps.apple.com/frame?map=hybrid&center=${lat}%2C${lon}&zoom=${zoom + 3}%2C${span}`, '_blank');
                if (layer.name === "Waze Live Map")
                    window.open(`https://www.waze.com/livemap?lat=${lat}&lng=${lon}&zoom=${zoom}`, '_blank');
                if (layer.name === "Mapillary")
                    window.open(`https://www.mapillary.com/app/?lat=${lat}&lng=${lon}&z=${zoom + 2}`, '_blank');
                // NOTE: ViaMichelin rebuilt their site (SvelteKit-based) and
                // the old /web/Maps?latitude=...&longitude=... route is
                // gone - it now 404s with "This page has been removed or
                // modified". Their current URLs are slug-based
                // (/maps/country/region/city/city-postcode), not raw
                // lat/lon, and there's no documented way to deep-link to
                // arbitrary coordinates anymore. Linking to the maps
                // landing page instead of a URL that's guaranteed to break.
                if (layer.name === "ViaMichelin")
                    window.open(`https://www.viamichelin.com/maps`, '_blank');
            });

            wrapper.appendChild(img);

            if (!layer.isButton && layer.name !== "Apple Maps" && layer.name !== "Mapillary") {
                const slider = document.createElement("input");
                slider.type = "range";
                slider.min = "0";
                slider.max = "1";
                slider.step = "0.01";
                slider.value = layer.initial;
                slider.style.width = "100%";
                slider.addEventListener("input", () => {
                    const value = parseFloat(slider.value);
                    sliderValues[layer.name] = value;
                    layer.onChange(value);

                    // While the panel is open, keep the underlying tile
                    // layer's visibility in sync with whether it's actually
                    // dialed in (value > 0). No point downloading tiles for
                    // a layer that's set to fully transparent.
                    if (isSliderVisible) {
                        const olLayer = tileLayers[layer.name] || (layer.name === "Traffic" ? trafficLayerRef : null);
                        if (olLayer) olLayer.setVisibility(value > 0);
                    }
                });
                wrapper.appendChild(slider);
            }

            sliderContainer.appendChild(wrapper);
        });

        const toggleButton = document.createElement("button");
        toggleButton.textContent = isSliderVisible ? "Hide Sliders" : "Show Sliders";
        toggleButton.style.position = "absolute";
        toggleButton.style.top = "37px";
        toggleButton.style.left = "50%";
        toggleButton.style.transform = "translateX(-50%)";
        toggleButton.style.zIndex = "2000";
        toggleButton.style.padding = "5px 10px";
        toggleButton.style.backgroundColor = "rgba(10, 25, 50, 0.8)";
        toggleButton.style.color = "white";
        toggleButton.style.border = "1px solid white";
        toggleButton.style.borderRadius = "5px";
        toggleButton.style.cursor = "pointer";

        toggleButton.addEventListener("click", () => {
            isSliderVisible = !isSliderVisible;
            sliderContainer.style.display = isSliderVisible ? "flex" : "none";
            toggleButton.textContent = isSliderVisible ? "Hide Sliders" : "Show Sliders";
            setBackgroundLoading(isSliderVisible);
        });

        document.body.appendChild(toggleButton);
        document.body.appendChild(sliderContainer);

        initTrafficLayer();
        initMapillaryLayer();
    }

    // Turns background tile downloading on/off for every managed layer.
    // - When switching ON: only layers whose slider is currently above 0
    //   resume fetching (no point turning on a layer nobody dialed in).
    // - When switching OFF: everything is forced to visibility:false,
    //   which stops OpenLayers from issuing any further tile requests,
    //   and the Mapillary iframe is pointed at about:blank to stop its
    //   own network activity too.
    function setBackgroundLoading(shouldLoad) {
        Object.keys(tileLayers).forEach(name => {
            const olLayer = tileLayers[name];
            if (!olLayer) return;
            olLayer.setVisibility(shouldLoad && sliderValues[name] > 0);
        });

        if (trafficLayerRef) {
            trafficLayerRef.setVisibility(shouldLoad && sliderValues["Traffic"] > 0);
        }

        if (mapillaryDiv && mapillaryIframe) {
            if (shouldLoad && sliderValues["Mapillary"] > 0) {
                updateMapillaryIframe();
            } else {
                mapillaryIframe.src = "about:blank";
            }
        }
    }

    function initTrafficLayer() {
        trafficLayerRef = new OpenLayers.Layer.XYZ(
            "Google Traffic",
            "https://mt1.google.com/vt?lyrs=h@159000000,traffic&hl=en&x=${x}&y=${y}&z=${z}",
            { isBaseLayer: false, opacity: 0.00, visibility: false }
        );
        W.map.addLayer(trafficLayerRef);
    }

    function updateMapillaryIframe() {
        const center = W.map.getCenter();
        const zoom = W.map.getZoom();
        const lonlat = new OpenLayers.LonLat(center.lon, center.lat);
        lonlat.transform(new OpenLayers.Projection("EPSG:900913"), new OpenLayers.Projection("EPSG:4326"));
        const lat = parseFloat(lonlat.lat.toFixed(6));
        const lon = parseFloat(lonlat.lon.toFixed(6));
        mapillaryIframe.src = `https://www.mapillary.com/app/?lat=${lat}&lng=${lon}&z=${zoom}`;
    }

    function initMapillaryLayer() {
        mapillaryDiv = document.createElement("div");
        mapillaryDiv.style.position = 'absolute';
        mapillaryDiv.style.top = '0';
        mapillaryDiv.style.left = '0';
        mapillaryDiv.style.right = '0';
        mapillaryDiv.style.bottom = '0';
        mapillaryDiv.style.zIndex = '400';
        mapillaryDiv.style.opacity = '0';
        mapillaryDiv.style.pointerEvents = 'none';

        mapillaryIframe = document.createElement("iframe");
        mapillaryIframe.style.width = '100%';
        mapillaryIframe.style.height = '100%';
        mapillaryIframe.style.border = 'none';
        // Start blank — no request fires until the panel is opened and the
        // Mapillary slider is actually moved above 0.
        mapillaryIframe.src = "about:blank";

        mapillaryDiv.appendChild(mapillaryIframe);
        W.map.olMap.getViewport().appendChild(mapillaryDiv);

        WazeWrap.Events.register('moveend', null, () => {
            if (isSliderVisible && sliderValues["Mapillary"] > 0) updateMapillaryIframe();
        });
        WazeWrap.Events.register('zoomend', null, () => {
            if (isSliderVisible && sliderValues["Mapillary"] > 0) updateMapillaryIframe();
        });
    }

    setTimeout(initOverlay, 2000);

})();