WME JSON IBGE Loader

Carrega a camada JSON/GeoJSON do IBGE diretamente no WME.

// ==UserScript==
// @name                WME JSON IBGE Loader
// @description         Carrega a camada JSON/GeoJSON do IBGE diretamente no WME.
// @namespace           [email protected]
// @version             1.0.2
// @author              T0NINI
// @include             https://www.waze.com/*/editor*
// @include             https://www.waze.com/editor*
// @include             https://beta.waze.com/*
// @exclude             https://www.waze.com/*user/*editor/*
// @icon                https://www.google.com/s2/favicons?sz=64&domain=waze.com
// @grant               none
// @require             https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
// ==/UserScript==

(function() {
    'use strict';

    const SCRIPT_VERSION = "1.0.2";

    let lineLayer = null;
    let clickControl = null;
    let selectedFeature = null;
    let isInitialized = false;

    function bootstrap() {
        if (typeof W === 'undefined' || typeof W.map === 'undefined' || typeof OpenLayers === 'undefined' || !WazeWrap.Ready) {
            setTimeout(bootstrap, 500);
            return;
        }
        initializeScript();
    }

    function initializeScript() {
        if (isInitialized) return;
        isInitialized = true;
        console.log(`JSON IBGE Loader: Script inicializado com sucesso! (v${SCRIPT_VERSION})`);
        createUI();
        createPopup();
        initializeHotkeyControls();
    }

    function createUI() {
        const tabTitle = 'JSON IBGE Loader';
        const navTabs = document.querySelector('#user-info .nav-tabs');
        const tabContent = document.querySelector('#user-info .tab-content');

        if (!navTabs || !tabContent || document.getElementById('sidepanel-jsonloader')) {
            return;
        }

        const newTab = document.createElement('li');
        newTab.innerHTML = `<a href="#sidepanel-jsonloader" data-toggle="tab">${tabTitle}</a>`;
        navTabs.appendChild(newTab);

        const newTabPanel = document.createElement('div');
        newTabPanel.id = 'sidepanel-jsonloader';
        newTabPanel.className = 'tab-pane';
        newTabPanel.innerHTML = `
            <style>
              #sidepanel-jsonloader .side-panel-section { padding: 10px; border-bottom: 1px solid #e5e5e5; }
              #sidepanel-jsonloader h4 { font-size: 28px; font-weight: 500; margin-bottom: 3px; }
              #sidepanel-jsonloader h5 { margin-top: 0; margin-bottom: 10px; font-size: 14px; }
              #sidepanel-jsonloader .w-100 { width: 100%; } #json-hotkey-input { cursor: pointer; }
            </style>
            <div class="side-panel-section"><h4>${tabTitle}</h4><p>Versão: <b>${SCRIPT_VERSION}</b></p></div>
            <div class="side-panel-section">
                <h5>Carregar Arquivo GeoJSON</h5>
                <input type="file" id="json-file-input" accept=".json,.geojson" class="form-control" />
                <p id="json-status"></p>
            </div>
            <div class="side-panel-section"><button id="json-clear-button" class="btn btn-danger w-100" disabled>Limpar Camada</button></div>
            <div class="side-panel-section">
                <h5>Ocultar Camada de Linhas (Atalho)</h5>
                <input type="text" id="json-hotkey-input" class="form-control" placeholder="Definir novo atalho" readonly>
                <p style="font-size: 11px; color: #888;">Pressione a tecla de atalho para mostrar/ocultar a camada. O padrão é <b>TAB</b>. Clique na caixa acima para definir um novo atalho.</p>
            </div>`;
        tabContent.appendChild(newTabPanel);

        document.getElementById('json-file-input').addEventListener('change', handleFileSelect);
        document.getElementById('json-clear-button').addEventListener('click', clearLayer);
    }

    function toggleLayerVisibility() {
        if (lineLayer) {
            lineLayer.setVisibility(!lineLayer.getVisibility());
        }
    }

    function initializeHotkeyControls() {
        const hotkeyInput = document.getElementById('json-hotkey-input');
        if(!hotkeyInput) return;
        const storageKey = 'wme_json_loader_hotkey';
        const defaultHotkey = 'TAB';
        hotkeyInput.value = localStorage.getItem(storageKey) || defaultHotkey;

        hotkeyInput.addEventListener('keydown', (event) => {
            event.preventDefault();
            let hotkeyString = [
                event.ctrlKey && 'Ctrl',
                event.altKey && 'Alt',
                event.shiftKey && 'Shift',
                !['Control', 'Alt', 'Shift'].includes(event.key) && event.key.toUpperCase()
            ].filter(Boolean).join(' + ');
            hotkeyInput.value = hotkeyString;
            localStorage.setItem(storageKey, hotkeyString);
        });

        document.addEventListener('keydown', (event) => {
            const activeEl = document.activeElement;
            if (activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA')) return;

            const targetHotkey = localStorage.getItem(storageKey) || defaultHotkey;
            const currentPress = [
                event.ctrlKey && 'Ctrl',
                event.altKey && 'Alt',
                event.shiftKey && 'Shift',
                !['Control', 'Alt', 'Shift'].includes(event.key) && event.key.toUpperCase()
            ].filter(Boolean).join(' + ');

            if (currentPress === targetHotkey) {
                event.preventDefault();
                toggleLayerVisibility();
            }
        });
    }

    function createPopup() {
        if (document.getElementById('wme-json-popup')) return;
        const styleSheet = document.createElement("style");
        styleSheet.type = "text/css";
        styleSheet.innerText = `
            #wme-json-popup {
                position: absolute; display: none; background: white; color: black; padding: 8px 12px;
                border-radius: 6px; border: 1px solid #ccc; z-index: 2001; font-family: sans-serif;
                font-size: 13px; font-weight: bold; pointer-events: none; transform: translate(-50%, -125%);
                white-space: nowrap; box-shadow: 0 3px 8px rgba(0,0,0,0.2);
            }
            #wme-json-popup::after, #wme-json-popup::before { content: ''; position: absolute; top: 100%; left: 50%;
                transform: translateX(-50%); border-style: solid; border-color: transparent; border-top-color: #ccc; }
            #wme-json-popup::after { border-width: 8px; margin-top: -1px; border-top-color: white; }
            #wme-json-popup::before { border-width: 9px; }`;
        document.head.appendChild(styleSheet);
        const popup = document.createElement('div');
        popup.id = 'wme-json-popup';
        document.getElementById('WazeMap').appendChild(popup);
    }

    function onFeatureSelect(feature, lonlat) {
        const popup = document.getElementById('wme-json-popup');
        const attr = feature.attributes;
        const nomeCompleto = [attr.NM_TIP_LOG, attr.NM_TIT_LOG, attr.NM_LOG].filter(Boolean).join(' ') || "Nome não disponível";
        popup.innerText = nomeCompleto;
        const pixel = W.map.getPixelFromLonLat(lonlat);
        popup.style.left = `${pixel.x}px`;
        popup.style.top = `${pixel.y}px`;
        popup.style.display = 'block';
    }

    function onFeatureUnselect() {
        const popup = document.getElementById('wme-json-popup');
        if (popup) popup.style.display = 'none';
        if (selectedFeature && lineLayer) {
            lineLayer.drawFeature(selectedFeature, 'default');
        }
        selectedFeature = null;
    }

    function initializeCustomClickHandler() {
        const OL = OpenLayers;
        const CustomClick = OL.Class(OL.Control, {
            defaultHandlerOptions: { 'single': true, 'double': false, 'pixelTolerance': 2, 'stopSingle': false, 'stopDouble': false },
            initialize: function(options) {
                this.handlerOptions = OL.Util.extend({}, this.defaultHandlerOptions);
                OL.Control.prototype.initialize.apply(this, arguments);
                this.handler = new OL.Handler.Click(this, { 'click': this.trigger }, this.handlerOptions);
            },
            trigger: function(e) {
                if (!lineLayer) return;
                const lonlat = W.map.getLonLatFromPixel(e.xy);
                const clickPoint = new OL.Geometry.Point(lonlat.lon, lonlat.lat).transform(new OL.Projection("EPSG:4326"), W.map.getProjectionObject());
                let closestFeature = null, minDistance = Infinity;

                for (const feature of lineLayer.features) {
                    const distance = feature.geometry.distanceTo(clickPoint, {details: false});
                    if (distance < minDistance) {
                        minDistance = distance;
                        closestFeature = feature;
                    }
                }
                if (closestFeature && minDistance < 3) {
                    if (selectedFeature !== closestFeature) {
                        onFeatureUnselect();
                        selectedFeature = closestFeature;
                        lineLayer.drawFeature(closestFeature, 'select');
                        onFeatureSelect(closestFeature, lonlat);
                    }
                }
            }
        });
        clickControl = new CustomClick();
        W.map.addControl(clickControl);
        clickControl.activate();
        W.map.events.register('movestart', null, onFeatureUnselect);
    }

    function handleFileSelect(event) {
        const file = event.target.files[0]; if (!file) return;
        const reader = new FileReader();
        const statusEl = document.getElementById('json-status');
        reader.onload = function(e) {
            statusEl.textContent = 'Analisando arquivo...';
            try {
                const geojson = JSON.parse(e.target.result);
                statusEl.textContent = `Arquivo válido. Desenhando ${geojson.features.length} linhas...`;
                setTimeout(() => drawGeoJSON(geojson), 50);
            } catch (error) {
                alert('Erro: O arquivo não parece ser um JSON válido.');
                console.error("Erro de Análise JSON:", error);
                clearLayer();
            }
        };
        reader.readAsText(file);
    }

    function drawGeoJSON(geojson) {
        const OL = OpenLayers;
        clearLayer();
        const sourceProjection = new OL.Projection("EPSG:4326");
        const mapProjection = W.map.getProjectionObject();
        const lineStyleMap = new OL.StyleMap({
            'default': new OL.Style({ strokeColor: '#0078FF', strokeWidth: 2, strokeOpacity: 0.8 }),
            'select': new OL.Style({ strokeColor: '#00FF00', strokeWidth: 4, strokeOpacity: 1.0 })
        });
        lineLayer = new OL.Layer.Vector("JSON Linhas", { styleMap: lineStyleMap });

        const allFeatures = [];
        for (const feature of geojson.features) {
            if (feature.geometry && feature.geometry.type === 'LineString' && feature.geometry.coordinates.length > 1) {
                const points = feature.geometry.coordinates.map(coord => new OL.Geometry.Point(coord[0], coord[1]));
                const line = new OL.Geometry.LineString(points);
                const vectorFeature = new OL.Feature.Vector(line, feature.properties);
                allFeatures.push(vectorFeature);
            }
        }

        allFeatures.forEach(f => f.geometry.transform(sourceProjection, mapProjection));
        lineLayer.addFeatures(allFeatures);
        W.map.addLayer(lineLayer);
        initializeCustomClickHandler();

        document.getElementById('json-status').textContent = `Camada desenhada com ${allFeatures.length} linhas.`;
        document.getElementById('json-clear-button').disabled = false;
    }

    function clearLayer() {
        onFeatureUnselect();
        if (clickControl) {
            W.map.events.unregister('movestart', null, onFeatureUnselect);
            clickControl.deactivate();
            W.map.removeControl(clickControl);
            clickControl = null;
        }
        if (lineLayer) {
            W.map.removeLayer(lineLayer);
            lineLayer.destroy();
            lineLayer = null;
        }
        selectedFeature = null;

        const status = document.getElementById('json-status');
        if (status) status.textContent = '';
        const fileInput = document.getElementById('json-file-input');
        if (fileInput) fileInput.value = '';
        const clearButton = document.getElementById('json-clear-button');
        if(clearButton) clearButton.disabled = true;
    }

    bootstrap();

})();