WME Closures Control

Permite controlar a exibição de interdições ativas, programadas e finalizadas na interface do WME.

スクリプトをインストールするには、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 Closures Control
// @description  Permite controlar a exibição de interdições ativas, programadas e finalizadas na interface do WME.
// @namespace    https://greasyfork.org/pt-BR/scripts/564733-wme-closures-control
// @version      1.2.0
// @icon         data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'><circle cx='32' cy='32' r='31' fill='%23000000'/><circle cx='32' cy='32' r='30' fill='%23ffffff'/><circle cx='32' cy='32' r='24' fill='%23d32f2f'/><rect x='14' y='28' width='36' height='8' rx='4' fill='%23ffffff'/></svg>
// @author       T0NINI
// @match        https://beta.waze.com/*editor*
// @match        https://www.waze.com/*editor*
// @exclude      https://www.waze.com/*user/*editor/*
// @require      https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const SCRIPT_NAME = 'WME_Closures_Control';

    const DEFAULT_SETTINGS = {
        showActive: true,
        showFuture: true,
        showInactive: false
    };

    let settings = { ...DEFAULT_SETTINGS };

    const UI_CONFIG = {
        itemHeight: '24px',
        fontSize: '14px',
        iconSize: '17px',
        dotSize: '9px',
        indentation: '2px',
        colorBlue: 'var(--primary_variant, #0075e3)',
        colorGray: 'var(--hairline_strong, #90959c)',
        colorText: 'var(--content_default, #202124)',
        colorBg:   'var(--background_default, #ffffff)',
        fontFamily: 'var(--wz-font-family, "Rubik", "Boing", sans-serif)'
    };

const TRANSLATIONS = {
        'pt-BR': { active: 'Ativas', scheduled: 'Programadas', finished: 'Finalizadas' },
        'en': { active: 'Active', scheduled: 'Scheduled', finished: 'Finished' },
        'es': { active: 'Activos', scheduled: 'Programados', finished: 'Finalizados' },
        'es-419': { active: 'Activos', scheduled: 'Programados', finished: 'Finalizados' },
        'fr': { active: 'Actifs', scheduled: 'Programmés', finished: 'Terminés' },
        'it': { active: 'Attivi', scheduled: 'Programmati', finished: 'Terminati' },
        'de': { active: 'Aktiv', scheduled: 'Geplant', finished: 'Beendet' },
        'nl': { active: 'Actief', scheduled: 'Gepland', finished: 'Voltooid' },
        'pl': { active: 'Aktywne', scheduled: 'Planowane', finished: 'Zakończone' },
        'ru': { active: 'Активные', scheduled: 'Плановые', finished: 'Завершенные' },
        'uk': { active: 'Активні', scheduled: 'Заплановані', finished: 'Завершені' },
        'cs': { active: 'Aktivní', scheduled: 'Plánované', finished: 'Ukončené' },
        'he': { active: 'פעיל', scheduled: 'מתוכנן', finished: 'הסתיים' },
        'ar': { active: 'نشط', scheduled: 'مجدول', finished: 'منتهي' }
    };

    function loadSettings() {
        try {
            const saved = localStorage.getItem(SCRIPT_NAME);
            if (saved) settings = { ...DEFAULT_SETTINGS, ...JSON.parse(saved) };
        } catch (e) {}
        applyBodyClasses();
    }

    function saveSettings() {
        localStorage.setItem(SCRIPT_NAME, JSON.stringify(settings));
        applyBodyClasses();
    }

    function applyBodyClasses() {
        document.body.classList.toggle('wme-hide-active', !settings.showActive);
        document.body.classList.toggle('wme-hide-future', !settings.showFuture);
        document.body.classList.toggle('wme-hide-inactive', !settings.showInactive);
        requestAnimationFrame(processMarkers);
    }

    function getClosureState(marker, element) {
        try {
            let realClosureId = null;
            if (marker.model?.attributes?.id) realClosureId = marker.model.attributes.id;
            else if (marker.feature?.attributes?.id) realClosureId = marker.feature.attributes.id;
            else if (marker.feature?.id) realClosureId = marker.feature.id;
            else if (element.dataset?.id) realClosureId = element.dataset.id;

            if (realClosureId && W?.model?.roadClosures) {
                if (realClosureId.includes('.')) realClosureId = realClosureId.split('.').pop();
                const closure = W.model.roadClosures.getObjectById(realClosureId);
                if (closure) {
                    const now = Date.now();
                    const startTime = new Date(closure.startTime).getTime();
                    const endTime = new Date(closure.endTime).getTime();
                    if (endTime < now) return 'inactive';
                    if (startTime > now) return 'future';
                    return 'active';
                }
            }

            const classNameStr = (element.className || "").toString();
            if (classNameStr.length < 5 || classNameStr === 'map-marker') return 'loading';
            if (classNameStr.includes('status-finished')) return 'inactive';
            if (
                classNameStr.includes('status-planned') ||
                classNameStr.includes('status-scheduled') ||
                classNameStr.includes('status-not-started')
            ) return 'future';
            if (element.style.opacity && parseFloat(element.style.opacity) < 0.9) return 'future';
        } catch (e) {
            return 'active';
        }
        return 'active';
    }

    function processMarkers() {
        if (!W?.map?.layers) return;
        const layer = W.map.layers.find(l => l.name === 'closures') || W.map.getLayersBy('name', 'Closures')[0];
        if (!layer || !layer.markers) return;

        for (const marker of layer.markers) {
            if (!marker) continue;
            const el = marker.element || marker.icon?.imageDiv;
            if (!el) continue;

            const state = getClosureState(marker, el);
            if (state === 'loading') continue;

            if (!el.classList.contains('wme-closure-marker')) el.classList.add('wme-closure-marker');
            if (el.dataset.wmeStatus !== state) el.dataset.wmeStatus = state;
        }
    }


    function injectStyles() {
        if (document.getElementById('wme-ui-fix-styles')) return;

        const css = `
            body.wme-hide-active .wme-closure-marker[data-wme-status="active"] { display:none!important; }
            body.wme-hide-future .wme-closure-marker[data-wme-status="future"] { display:none!important; }
            body.wme-hide-inactive .wme-closure-marker[data-wme-status="inactive"] { display:none!important; }

            body.wme-hide-inactive .map-marker.status-finished { display:none!important; }

            body.wme-hide-future .map-marker.status-planned,
            body.wme-hide-future .map-marker.status-scheduled,
            body.wme-hide-future .map-marker.status-not-started { display:none!important; }

            .wme-ui-fix-submenu {
                list-style:none;
                padding:0;
                margin:0;
                transition: opacity 0.2s, max-height 0.2s;
                overflow: hidden;
                max-height: 200px;
                opacity: 1;
            }

            .wme-ui-fix-submenu.layer-off {
                max-height: 0;
                opacity: 0;
                margin: 0;
                pointer-events: none;
            }

            .wme-ui-fix-item {
                display:flex;
                align-items:center;
                height:${UI_CONFIG.itemHeight};
                padding-left:${UI_CONFIG.indentation};
                gap:10px;
                cursor:pointer;
                font-family:${UI_CONFIG.fontFamily};
                font-size:${UI_CONFIG.fontSize};
                color:${UI_CONFIG.colorText};
                user-select:none;
            }

            .wme-ui-fix-icon {
                width:${UI_CONFIG.iconSize};
                height:${UI_CONFIG.iconSize};
                min-width:${UI_CONFIG.iconSize};
                border:1px solid ${UI_CONFIG.colorGray};
                border-radius:50%;
                background:${UI_CONFIG.colorBg};
                position:relative;
                box-sizing:border-box;
            }

            .wme-ui-fix-item.checked .wme-ui-fix-icon {
                border-color:${UI_CONFIG.colorBlue};
            }

            .wme-ui-fix-item.checked .wme-ui-fix-icon::after {
                content:'';
                position:absolute;
                top:50%;
                left:50%;
                transform:translate(-50%,-50%);
                width:${UI_CONFIG.dotSize};
                height:${UI_CONFIG.dotSize};
                background:${UI_CONFIG.colorBlue};
                border-radius:50%;
            }
            @keyframes uiFadeIn {
                from { opacity:0; transform:translateY(-3px); }
                to { opacity:1; transform:translateY(0); }
            }
        `;

        const style = document.createElement('style');
        style.id = 'wme-ui-fix-styles';
        style.textContent = css;
        document.head.appendChild(style);
    }

    function buildMenu() {
        const ul = document.createElement('ul');
        ul.className = 'wme-ui-fix-submenu';

        const lang = I18n.locale || 'en';
        const strings = TRANSLATIONS[lang] || TRANSLATIONS.en;

        const createItem = (label, key) => {
            const li = document.createElement('li');
            li.className = `wme-ui-fix-item ${settings[key] ? 'checked' : ''}`;
            li.innerHTML = `<div class="wme-ui-fix-icon"></div><span>${label}</span>`;
            li.onclick = e => {
                e.stopPropagation();
                settings[key] = li.classList.toggle('checked');
                saveSettings();
            };
            return li;
        };

        ul.appendChild(createItem(strings.active, 'showActive'));
        ul.appendChild(createItem(strings.scheduled, 'showFuture'));
        ul.appendChild(createItem(strings.finished, 'showInactive'));

        return ul;
    }

    function findClosureLayerLi() {
        const directId = document.getElementById('layer-switcher-item_closures');
        if (directId) return directId.closest('li');
        return null;
    }

    function syncMenuVisibility(checkbox, menuUl) {
        if (!checkbox || !menuUl) return;
        const isChecked = checkbox.checked || checkbox.hasAttribute('checked');
        if (isChecked) {
            menuUl.classList.remove('layer-off');
        } else {
            menuUl.classList.add('layer-off');
        }
    }

    function attemptIntegration() {
        try {
            const li = findClosureLayerLi();
            if (!li || li.querySelector('.wme-ui-fix-submenu')) return;

            const menu = buildMenu();
            li.appendChild(menu);

            const nativeCheckbox = document.getElementById('layer-switcher-item_closures');
            if (nativeCheckbox) {
                syncMenuVisibility(nativeCheckbox, menu);
                nativeCheckbox.addEventListener('click', () => {
                    setTimeout(() => syncMenuVisibility(nativeCheckbox, menu), 50);
                });
                const attrObserver = new MutationObserver(() => {
                    syncMenuVisibility(nativeCheckbox, menu);
                });
                attrObserver.observe(nativeCheckbox, { attributes: true, attributeFilter: ['checked'] });
            }
        } catch (e) {
            console.error('WME Closures Control: Erro na integração', e);
        }
    }

    function init() {
        injectStyles();
        loadSettings();

        const observer = new MutationObserver(muts => {
            attemptIntegration();
        });

        observer.observe(document.getElementById('sidebar') || document.body, {
            childList: true,
            subtree: true
        });

        W.map.events.register('moveend', null, processMarkers);
        W.map.events.register('zoomend', null, processMarkers);

        setInterval(processMarkers, 2500);

        attemptIntegration();
    }

    function bootstrap() {
        if (window.WazeWrap?.Interface?.onWmeReady) {
            WazeWrap.Interface.onWmeReady(init);
        } else {
            const wait = setInterval(() => {
                if (window.W && window.W.map && window.I18n) {
                    clearInterval(wait);
                    init();
                }
            }, 250);
        }
    }

    bootstrap();

})();