Mudanças entre as edições de "Widget:MapViewer.js"

De Wiki Gla
Ir para navegação Ir para pesquisar
 
(47 revisões intermediárias pelo mesmo usuário não estão sendo mostradas)
Linha 1: Linha 1:
/**
<includeonly><div id="mapa-viewer-<!--{$id|escape:'quotes'|default:'mapa1'}-->" style="width:<!--{$largura|escape:'quotes'|default:'100%'}-->; height:<!--{$altura|escape:'quotes'|default:'500px'}-->; background:#0f172a; border-radius:12px; overflow:hidden; position:relative;">
* MapViewer Widget para MediaWiki
    <div style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); color:#64748b; text-align:center;">
* Uso: {{#widget:MapViewer.js|config=JSON_do_mapa|width=800|height=600}}
        🗺️ Carregando mapa...
*/
    </div>
</div>


<script>
// @noescape
(function() {
(function() {
     // Verificar se já foi carregado
     var id = '<!--{$id|escape:'quotes'|default:'mapa1'}-->';
     if (window.MWMapViewerLoaded) return;
     var container = document.getElementById('mapa-viewer-' + id);
     window.MWMapViewerLoaded = true;
     if (!container) return;
      
      
     // Estilos do widget
     // JSON vem URL encoded para não quebrar o parser
     const styles = `
     var jsonEncoded = '<!--{$json|escape:'quotes'}-->';
        <style>
    var jsonString = decodeURIComponent(jsonEncoded);
            .mw-map-viewer {
   
                position: relative;
    var mapConfig;
                background: #0f172a;
    try {
                border-radius: 12px;
        mapConfig = JSON.parse(jsonString);
                overflow: hidden;
    } catch(e) {
                box-shadow: 0 4px 20px rgba(0,0,0,0.2);
        container.innerHTML = '<div style="padding:20px; text-align:center; color:#ef4444;">❌ Erro no JSON: ' + e.message + '</div>';
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
        return;
            }
    }
            .mw-map-viewer .map-viewport {
   
                width: 100%;
    if (!mapConfig.layers || mapConfig.layers.length === 0) {
                height: 100%;
        container.innerHTML = '<div style="padding:20px; text-align:center; color:#f59e0b;">⚠️ Nenhuma camada configurada</div>';
                overflow: auto;
        return;
                cursor: grab;
    }
                background: #0f172a;
   
            }
    // Iniciar o visualizador
            .mw-map-viewer .map-viewport:active {
    iniciarMapaViewer(container, mapConfig);
                cursor: grabbing;
            }
            .mw-map-viewer .map-layers {
                position: relative;
                min-width: 100%;
                min-height: 100%;
            }
            .mw-map-viewer .map-layer {
                position: absolute;
                top: 0;
                left: 0;
                transform-origin: 0 0;
            }
            .mw-map-viewer .map-image {
                display: block;
                transform-origin: 0 0;
                pointer-events: none;
                border-radius: 4px;
            }
            .mw-map-viewer .map-marker {
                position: absolute;
                cursor: pointer;
                z-index: 100;
                transform: translate(-50%, -50%);
                transition: transform 0.1s ease;
            }
            .mw-map-viewer .map-marker:hover {
                transform: translate(-50%, -50%) scale(1.15);
                z-index: 102;
            }
            .mw-map-viewer .marker-icon {
                display: flex;
                align-items: center;
                justify-content: center;
                filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));
            }
            .mw-map-viewer .marker-tooltip {
                position: absolute;
                bottom: 100%;
                left: 50%;
                transform: translateX(-50%) translateY(-8px);
                background: #1e293b;
                color: #e2e8f0;
                padding: 4px 10px;
                border-radius: 8px;
                font-size: 11px;
                white-space: nowrap;
                opacity: 0;
                visibility: hidden;
                transition: all 0.2s;
                pointer-events: none;
                box-shadow: 0 2px 8px rgba(0,0,0,0.2);
                border: 1px solid #334155;
            }
            .mw-map-viewer .map-marker:hover .marker-tooltip {
                opacity: 1;
                visibility: visible;
                transform: translateX(-50%) translateY(-12px);
            }
            .mw-map-viewer .marker-badge {
                position: absolute;
                top: -8px;
                right: -8px;
                background: #ef4444;
                color: white;
                font-size: 9px;
                font-weight: bold;
                min-width: 16px;
                height: 16px;
                border-radius: 10px;
                display: flex;
                align-items: center;
                justify-content: center;
                padding: 0 4px;
                font-family: monospace;
                box-shadow: 0 1px 3px rgba(0,0,0,0.2);
            }
            .mw-map-viewer .marker-badge.hidden {
                display: none;
            }
            .mw-map-viewer .viewer-toolbar {
                position: absolute;
                top: 12px;
                left: 12px;
                right: 12px;
                display: flex;
                justify-content: space-between;
                z-index: 101;
                gap: 10px;
                flex-wrap: wrap;
            }
            .mw-map-viewer .viewer-controls {
                display: flex;
                gap: 5px;
                background: rgba(0,0,0,0.6);
                backdrop-filter: blur(8px);
                padding: 5px 10px;
                border-radius: 40px;
            }
            .mw-map-viewer .viewer-controls button {
                background: #334155;
                border: none;
                color: white;
                width: 32px;
                height: 32px;
                border-radius: 50%;
                cursor: pointer;
                font-size: 16px;
                transition: all 0.2s;
            }
            .mw-map-viewer .viewer-controls button:hover {
                background: #6366f1;
                transform: scale(1.05);
            }
            .mw-map-viewer .floor-info {
                background: rgba(0,0,0,0.6);
                backdrop-filter: blur(8px);
                padding: 5px 15px;
                border-radius: 40px;
                color: white;
                font-size: 12px;
            }
            .mw-map-viewer .zoom-level {
                background: rgba(0,0,0,0.6);
                backdrop-filter: blur(8px);
                padding: 5px 12px;
                border-radius: 40px;
                color: #a5b4fc;
                font-size: 12px;
                font-family: monospace;
            }
            .mw-map-viewer .coordinates-panel {
                position: absolute;
                bottom: 10px;
                right: 10px;
                background: rgba(0,0,0,0.6);
                backdrop-filter: blur(8px);
                padding: 4px 10px;
                border-radius: 20px;
                color: #a5b4fc;
                font-size: 10px;
                font-family: monospace;
                z-index: 100;
                pointer-events: none;
            }
            .mw-map-viewer .nav-controls {
                position: absolute;
                bottom: 20px;
                left: 20px;
                display: flex;
                gap: 8px;
                z-index: 101;
            }
            .mw-map-viewer .nav-controls button {
                background: rgba(0,0,0,0.7);
                backdrop-filter: blur(8px);
                border: none;
                color: white;
                width: 40px;
                height: 40px;
                border-radius: 50%;
                cursor: pointer;
                font-size: 18px;
                transition: all 0.2s;
            }
            .mw-map-viewer .nav-controls button:hover {
                background: #6366f1;
                transform: scale(1.05);
            }
            .mw-map-viewer-popup {
                display: none;
                position: fixed;
                z-index: 10000;
                left: 0;
                top: 0;
                width: 100%;
                height: 100%;
                background: rgba(0,0,0,0.7);
                justify-content: center;
                align-items: center;
            }
            .mw-map-viewer-popup .popup-content {
                background: #1e293b;
                border-radius: 16px;
                max-width: 350px;
                width: 90%;
                padding: 20px;
                position: relative;
                border: 1px solid #334155;
                animation: popupSlide 0.3s ease;
            }
            @keyframes popupSlide {
                from { transform: translateY(-30px); opacity: 0; }
                to { transform: translateY(0); opacity: 1; }
            }
            .mw-map-viewer-popup .popup-close {
                position: absolute;
                right: 15px;
                top: 10px;
                font-size: 24px;
                cursor: pointer;
                color: #94a3b8;
            }
            .mw-map-viewer-popup .popup-close:hover {
                color: #ef4444;
            }
            .mw-map-viewer-popup h3 {
                color: #a5b4fc;
                margin-bottom: 12px;
            }
            .mw-map-viewer-popup p {
                color: #cbd5e1;
                line-height: 1.5;
                font-size: 14px;
            }
            .mw-map-viewer-popup img {
                max-width: 100%;
                border-radius: 12px;
                margin-bottom: 16px;
            }
            @media (max-width: 768px) {
                .mw-map-viewer .viewer-controls button {
                    width: 28px;
                    height: 28px;
                    font-size: 14px;
                }
                .mw-map-viewer .floor-info {
                    font-size: 10px;
                    padding: 4px 12px;
                }
                .mw-map-viewer .zoom-level {
                    font-size: 10px;
                    padding: 4px 10px;
                }
            }
        </style>
    `;
      
      
     class MWMapViewer {
     function iniciarMapaViewer(container, config) {
        constructor(container, config, options = {}) {
        container.innerHTML = '';
            this.container = container;
        container.style.position = 'relative';
            this.config = this.normalizeConfig(config);
        container.style.overflow = 'hidden';
            this.width = options.width || '100%';
       
            this.height = options.height || '500px';
        // Toolbar
           
        var toolbar = document.createElement('div');
            this.currentZoom = this.config.mapConfig?.defaultZoom || 1;
        toolbar.style.cssText = 'position:absolute; top:10px; left:10px; right:10px; z-index:100; display:flex; justify-content:space-between; gap:8px; flex-wrap:wrap;';
            this.currentFloor = this.config.mapConfig?.initialFloor || 0;
        toolbar.innerHTML = '<div style="display:flex; gap:5px; background:rgba(0,0,0,0.7); padding:5px 10px; border-radius:30px;">' +
             this.minZoom = this.config.mapConfig?.minZoom || 0.5;
             '<button id="zoom-in-' + id + '" style="background:#334155; border:none; color:white; width:32px; height:32px; border-radius:50%; cursor:pointer;">+</button>' +
             this.maxZoom = this.config.mapConfig?.maxZoom || 3;
             '<button id="zoom-out-' + id + '" style="background:#334155; border:none; color:white; width:32px; height:32px; border-radius:50%; cursor:pointer;">-</button>' +
             this.zoomStep = this.config.mapConfig?.zoomStep || 0.1;
             '<button id="reset-' + id + '" style="background:#334155; border:none; color:white; width:32px; height:32px; border-radius:50%; cursor:pointer;">⟳</button>' +
              
             '</div>' +
             this.layers = [];
             '<div style="background:rgba(0,0,0,0.7); padding:5px 15px; border-radius:30px; color:white; font-size:12px;"><span id="floor-name-' + id + '">' + (config.layers[0]?.name || 'Mapa') + '</span></div>' +
            this.markers = [];
             '<div id="zoom-level-' + id + '" style="background:rgba(0,0,0,0.7); padding:5px 12px; border-radius:30px; color:#a5b4fc; font-size:12px;">100%</div>';
             this.isPanning = false;
            this.panStart = { x: 0, y: 0, scrollLeft: 0, scrollTop: 0 };
           
            this.init();
        }
          
          
         normalizeConfig(config) {
         // Viewport
            if (!config.mapConfig) {
        var viewport = document.createElement('div');
                config.mapConfig = { initialFloor: 0, defaultZoom: 1, minZoom: 0.5, maxZoom: 3, zoomStep: 0.1 };
        viewport.style.cssText = 'width:100%; height:100%; overflow:auto; cursor:grab; background:#0f172a;';
            }
        var camadasDiv = document.createElement('div');
            if (!config.layers) config.layers = [];
        camadasDiv.style.cssText = 'position:relative; min-width:100%; min-height:100%;';
           
         viewport.appendChild(camadasDiv);
            config.layers.forEach(layer => {
                if (!layer.alignment) layer.alignment = { offsetX: 0, offsetY: 0, scale: 1 };
                if (layer.opacity === undefined) layer.opacity = 100;
                if (!layer.markers) layer.markers = [];
               
                layer.markers.forEach(marker => {
                    if (!marker.actionData) marker.actionData = {};
                    if (marker.action === 'gotoFloor' && marker.actionData.floorId === undefined) marker.actionData.floorId = 0;
                    if (marker.action === 'link' && !marker.actionData.url) marker.actionData.url = '#';
                    if (marker.action === 'popup' && !marker.actionData.text) marker.actionData.text = 'Sem informações';
                });
            });
            return config;
         }
          
          
         init() {
         // Navegação
            this.createDOM();
        var navDiv = document.createElement('div');
            this.renderLayers();
        navDiv.style.cssText = 'position:absolute; bottom:20px; left:20px; z-index:100; display:flex; gap:8px; background:rgba(0,0,0,0.7); padding:8px; border-radius:40px;';
            this.setupEvents();
        navDiv.innerHTML = '<button id="prev-' + id + '" style="background:#334155; border:none; color:white; width:36px; height:36px; border-radius:50%; cursor:pointer;">▲</button>' +
             this.updateMarkersPosition();
             '<button id="next-' + id + '" style="background:#334155; border:none; color:white; width:36px; height:36px; border-radius:50%; cursor:pointer;">▼</button>';
            this.initPopup();
        }
          
          
         createDOM() {
         // Coordenadas
            this.container.innerHTML = '';
        var coordsDiv = document.createElement('div');
            this.container.className = 'mw-map-viewer';
        coordsDiv.style.cssText = 'position:absolute; bottom:10px; right:10px; background:rgba(0,0,0,0.6); padding:4px 10px; border-radius:20px; color:#a5b4fc; font-size:10px; font-family:monospace;';
            this.container.style.width = this.width;
        coordsDiv.textContent = '📍 0, 0';
            this.container.style.height = this.height;
            this.container.style.position = 'relative';
            this.container.style.overflow = 'hidden';
           
            this.container.innerHTML = `
                <div class="viewer-toolbar">
                    <div class="viewer-controls">
                        <button class="mw-zoom-in" title="Zoom In (+ / Ctrl+Scroll)">+</button>
                        <button class="mw-zoom-out" title="Zoom Out (- / Ctrl+Scroll)">-</button>
                        <button class="mw-reset" title="Resetar Visualização (R)">⟳</button>
                    </div>
                    <div class="floor-info">
                        <span class="mw-floor-name">Carregando...</span>
                    </div>
                    <div class="zoom-level">
                        <span class="mw-zoom-level">100%</span>
                    </div>
                </div>
                <div class="map-viewport">
                    <div class="map-layers"></div>
                </div>
                <div class="coordinates-panel">
                    <span class="mw-coords">📍 0, 0</span>
                </div>
                <div class="nav-controls">
                    <button class="mw-prev-floor" title="Andar Anterior (PageUp)">▲</button>
                    <button class="mw-next-floor" title="Próximo Andar (PageDown)">▼</button>
                </div>
            `;
           
            this.viewport = this.container.querySelector('.map-viewport');
            this.layersContainer = this.container.querySelector('.map-layers');
            this.floorNameSpan = this.container.querySelector('.mw-floor-name');
            this.zoomLevelSpan = this.container.querySelector('.mw-zoom-level');
            this.coordsSpan = this.container.querySelector('.mw-coords');
           
            // Bind events
            this.container.querySelector('.mw-zoom-in')?.addEventListener('click', () => this.zoomIn());
            this.container.querySelector('.mw-zoom-out')?.addEventListener('click', () => this.zoomOut());
            this.container.querySelector('.mw-reset')?.addEventListener('click', () => this.resetView());
            this.container.querySelector('.mw-prev-floor')?.addEventListener('click', () => this.prevFloor());
            this.container.querySelector('.mw-next-floor')?.addEventListener('click', () => this.nextFloor());
           
            this.setupInteractions();
            this.setupKeyboard();
        }
          
          
         setupInteractions() {
         container.appendChild(toolbar);
            // Pan
        container.appendChild(viewport);
            this.viewport.addEventListener('mousedown', (e) => {
        container.appendChild(coordsDiv);
                if (e.target.closest('.map-marker')) return;
        container.appendChild(navDiv);
                this.isPanning = true;
                this.panStart = {
                    x: e.clientX,
                    y: e.clientY,
                    scrollLeft: this.viewport.scrollLeft,
                    scrollTop: this.viewport.scrollTop
                };
                this.viewport.style.cursor = 'grabbing';
                e.preventDefault();
            });
           
            window.addEventListener('mousemove', (e) => {
                if (!this.isPanning) return;
                this.viewport.scrollLeft = this.panStart.scrollLeft - (e.clientX - this.panStart.x);
                this.viewport.scrollTop = this.panStart.scrollTop - (e.clientY - this.panStart.y);
            });
           
            window.addEventListener('mouseup', () => {
                this.isPanning = false;
                this.viewport.style.cursor = 'grab';
            });
           
            // Zoom com scroll
            this.viewport.addEventListener('wheel', (e) => {
                if (e.ctrlKey) {
                    e.preventDefault();
                    e.deltaY < 0 ? this.zoomIn() : this.zoomOut();
                }
            });
           
            // Coordenadas
            this.viewport.addEventListener('mousemove', (e) => {
                const rect = this.viewport.getBoundingClientRect();
                const x = (e.clientX - rect.left + this.viewport.scrollLeft) / this.currentZoom;
                const y = (e.clientY - rect.top + this.viewport.scrollTop) / this.currentZoom;
                if (this.coordsSpan) {
                    this.coordsSpan.textContent = `📍 ${Math.round(x)}, ${Math.round(y)} | ${Math.round(this.currentZoom * 100)}%`;
                }
            });
        }
          
          
         setupKeyboard() {
         // Variáveis
            const handler = (e) => {
        var zoomAtual = config.mapConfig.defaultZoom || 1;
                if (e.target.tagName === 'INPUT') return;
        var andarAtual = config.mapConfig.initialFloor || 0;
                switch(e.key) {
        var zoomMin = config.mapConfig.minZoom || 0.5;
                    case '+': case '=': e.preventDefault(); this.zoomIn(); break;
        var zoomMax = config.mapConfig.maxZoom || 3;
                    case '-': case '_': e.preventDefault(); this.zoomOut(); break;
        var zoomPasso = config.mapConfig.zoomStep || 0.1;
                    case 'r': case 'R': e.preventDefault(); this.resetView(); break;
        var camadas = [];
                    case 'ArrowUp': case 'PageUp': e.preventDefault(); this.prevFloor(); break;
        var marcadores = [];
                    case 'ArrowDown': case 'PageDown': e.preventDefault(); this.nextFloor(); break;
        var arrastando = false;
                }
         var arrasteInicio = { x: 0, y: 0, scrollLeft: 0, scrollTop: 0 };
            };
            document.addEventListener('keydown', handler);
         }
          
          
         renderLayers() {
         function renderizarCamadas() {
             if (!this.layersContainer) return;
             camadasDiv.innerHTML = '';
            this.layersContainer.innerHTML = '';
             camadas = [];
             this.layers = [];
             marcadores = [];
             this.markers = [];
              
              
             if (!this.config.layers || this.config.layers.length === 0) {
             if (!config.layers || config.layers.length === 0) return;
                this.layersContainer.innerHTML = '<div style="text-align:center; padding:50px; color:#64748b;">Nenhuma camada carregada</div>';
                return;
            }
              
              
             if (!this.config.layers.find(l => l.id === this.currentFloor)) {
             var andarExiste = false;
                 this.currentFloor = this.config.layers[0]?.id || 0;
            for (var i = 0; i < config.layers.length; i++) {
                 if (config.layers[i].id === andarAtual) andarExiste = true;
             }
             }
            if (!andarExiste && config.layers.length > 0) andarAtual = config.layers[0].id;
              
              
             const currentFloorData = this.config.layers.find(l => l.id === this.currentFloor);
             var andarInfo = null;
            if (currentFloorData && this.floorNameSpan) {
            for (var i = 0; i < config.layers.length; i++) {
                this.floorNameSpan.textContent = currentFloorData.name;
                if (config.layers[i].id === andarAtual) andarInfo = config.layers[i];
             }
             }
            var nomeSpan = document.getElementById('floor-name-' + id);
            if (nomeSpan && andarInfo) nomeSpan.textContent = andarInfo.name;
              
              
             this.config.layers.forEach((layer) => {
             for (var i = 0; i < config.layers.length; i++) {
                 const $layer = document.createElement('div');
                 var layer = config.layers[i];
                 $layer.className = 'map-layer';
                var divLayer = document.createElement('div');
                 $layer.setAttribute('data-floor', layer.id);
                 divLayer.className = 'mapa-layer';
                 $layer.style.display = layer.id === this.currentFloor ? 'block' : 'none';
                 divLayer.setAttribute('data-floor', layer.id);
                 $layer.style.transform = `translate(${layer.alignment?.offsetX || 0}px, ${layer.alignment?.offsetY || 0}px)`;
                 divLayer.style.display = layer.id === andarAtual ? 'block' : 'none';
                 $layer.style.opacity = (layer.opacity || 100) / 100;
                 divLayer.style.position = 'absolute';
                divLayer.style.top = '0';
                divLayer.style.left = '0';
                divLayer.style.transform = 'translate(' + (layer.alignment?.offsetX || 0) + 'px, ' + (layer.alignment?.offsetY || 0) + 'px)';
                 divLayer.style.opacity = (layer.opacity || 100) / 100;
                  
                  
                 const $img = document.createElement('img');
                 var img = document.createElement('img');
                 $img.className = 'map-image';
                 img.style.display = 'block';
                 $img.style.transform = `scale(${this.currentZoom})`;
                 img.style.transform = 'scale(' + zoomAtual + ')';
                 $img.style.transformOrigin = '0 0';
                 img.style.transformOrigin = '0 0';
                img.src = layer.imagePath || '';
                img.onload = function() { camadasDiv.style.width = this.width + 'px'; camadasDiv.style.height = this.height + 'px'; };
                img.onerror = function() { this.src = 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22800%22 height=%22600%22%3E%3Crect width=%22100%25%22 height=%22100%25%22 fill=%22%23334155%22/%3E%3Ctext x=%2250%25%22 y=%2250%25%22 text-anchor=%22middle%22 fill=%22white%22%3ESem imagem%3C/text%3E%3C/svg%3E'; };
                divLayer.appendChild(img);
                  
                  
                 if (layer.imagePath) {
                 var markersDiv = document.createElement('div');
                    $img.src = layer.imagePath;
                markersDiv.style.position = 'absolute';
                } else {
                markersDiv.style.top = '0';
                    const canvas = document.createElement('canvas');
                markersDiv.style.left = '0';
                    canvas.width = 800;
                markersDiv.style.width = '100%';
                    canvas.height = 600;
                markersDiv.style.height = '100%';
                    const ctx = canvas.getContext('2d');
                markersDiv.style.pointerEvents = 'none';
                    ctx.fillStyle = '#334155';
                    ctx.fillRect(0, 0, canvas.width, canvas.height);
                    ctx.fillStyle = 'white';
                    ctx.font = '20px Arial';
                    ctx.fillText(layer.name || `Andar ${layer.id + 1}`, 50, 100);
                    $img.src = canvas.toDataURL();
                }
                  
                  
                 $img.onload = () => {
                 if (layer.markers) {
                     this.layersContainer.style.width = `${$img.width}px`;
                     for (var j = 0; j < layer.markers.length; j++) {
                    this.layersContainer.style.height = `${$img.height}px`;
                        var marker = layer.markers[j];
                };
                         var markerDiv = criarMarcador(marker, layer.id);
               
                         markersDiv.appendChild(markerDiv);
                $layer.appendChild($img);
                         marcadores.push({ el: markerDiv, data: marker });
               
                     }
                const $markersContainer = document.createElement('div');
                $markersContainer.style.position = 'absolute';
                $markersContainer.style.top = '0';
                $markersContainer.style.left = '0';
                $markersContainer.style.width = '100%';
                $markersContainer.style.height = '100%';
                $markersContainer.style.pointerEvents = 'none';
               
                if (layer.markers && layer.markers.length > 0) {
                    layer.markers.forEach(marker => {
                         const $marker = this.createMarker(marker, layer.id);
                         $markersContainer.appendChild($marker);
                         this.markers.push({ element: $marker, data: marker });
                     });
                 }
                 }
                  
                 divLayer.appendChild(markersDiv);
                $layer.appendChild($markersContainer);
                 camadasDiv.appendChild(divLayer);
                 this.layersContainer.appendChild($layer);
                 camadas.push(divLayer);
                 this.layers.push($layer);
             }
             });
         }
         }
          
          
         createMarker(marker, floorId) {
         function criarMarcador(marker, floorId) {
             const $marker = document.createElement('div');
             var div = document.createElement('div');
             $marker.className = 'map-marker';
             div.className = 'mapa-marker';
             $marker.setAttribute('data-marker-id', marker.id);
             div.setAttribute('data-marker-id', marker.id);
             $marker.setAttribute('data-floor', floorId);
             div.setAttribute('data-floor', floorId);
             $marker.setAttribute('data-x', marker.x);
             div.setAttribute('data-x', marker.x);
             $marker.setAttribute('data-y', marker.y);
             div.setAttribute('data-y', marker.y);
            div.style.position = 'absolute';
            div.style.left = (marker.x * zoomAtual) + 'px';
            div.style.top = (marker.y * zoomAtual) + 'px';
            div.style.zIndex = '100';
            div.style.transform = 'translate(-50%, -50%)';
            div.style.cursor = (marker.action && marker.action !== 'none') ? 'pointer' : 'default';
              
              
             $marker.style.position = 'absolute';
             var tamanhoIcone = Math.max(20, Math.floor(20 * zoomAtual));
             $marker.style.left = `${marker.x * this.currentZoom}px`;
             var htmlIcone = marker.iconBase64 ? '<img src="' + marker.iconBase64 + '" style="width:' + tamanhoIcone + 'px; height:' + tamanhoIcone + 'px;">' : '<span style="font-size:' + tamanhoIcone + 'px;">📍</span>';
            $marker.style.top = `${marker.y * this.currentZoom}px`;
            $marker.style.zIndex = '100';
            $marker.style.transform = 'translate(-50%, -50%)';
              
              
             const hasAction = marker.action && marker.action !== 'none';
             var textoAcao = '';
             $marker.style.cursor = hasAction ? 'pointer' : 'default';
            if (marker.action === 'popup') textoAcao = '📋 Informações';
            else if (marker.action === 'nextFloor') textoAcao = '⬆️ Próximo andar';
             else if (marker.action === 'prevFloor') textoAcao = '⬇️ Andar anterior';
            else if (marker.action === 'gotoFloor') textoAcao = '🎯 Ir para andar';
            else if (marker.action === 'link') textoAcao = '🔗 Link externo';
            else textoAcao = '📍 Clique';
              
              
             const iconSize = Math.max(20, Math.floor(20 * this.currentZoom));
             div.innerHTML = '<div style="display:flex; align-items:center; justify-content:center;">' + htmlIcone + '</div>' +
            let iconHtml = marker.iconBase64 ?
                '<div style="position:absolute; bottom:100%; left:50%; transform:translateX(-50%); background:#1e293b; color:white; padding:4px 8px; border-radius:6px; font-size:10px; white-space:nowrap; opacity:0; visibility:hidden; transition:0.2s; pointer-events:none;">' +
                `<img src="${marker.iconBase64}" style="width:${iconSize}px; height:${iconSize}px;">` :
                '<strong>' + (marker.name || 'Marcador') + '</strong><br><small>' + textoAcao + '</small></div>' +
                 `<span style="font-size:${iconSize}px;">📍</span>`;
                 '<div style="position:absolute; top:-8px; right:-8px; background:#ef4444; color:white; font-size:9px; min-width:16px; height:16px; border-radius:10px; display:flex; align-items:center; justify-content:center; padding:0 4px; ' + (!marker.hasBadge ? 'display:none;' : '') + '">' + (marker.hasBadge ? (marker.badgeNumber || marker.number || '') : '') + '</div>';
              
              
             let actionText = '';
             div.addEventListener('mouseenter', function() { this.style.transform = 'translate(-50%, -50%) scale(1.15)'; var tp = this.children[1]; if (tp) { tp.style.opacity = '1'; tp.style.visibility = 'visible'; } });
            switch(marker.action) {
            div.addEventListener('mouseleave', function() { this.style.transform = 'translate(-50%, -50%) scale(1)'; var tp = this.children[1]; if (tp) { tp.style.opacity = '0'; tp.style.visibility = 'hidden'; } });
                case 'popup': actionText = '📋 Informações'; break;
                case 'nextFloor': actionText = '⬆️ Próximo andar'; break;
                case 'prevFloor': actionText = '⬇️ Andar anterior'; break;
                case 'gotoFloor': actionText = '🎯 Ir para andar'; break;
                case 'link': actionText = '🔗 Link externo'; break;
                default: actionText = '📍 Clique';
            }
              
              
             $marker.innerHTML = `
             if (marker.action && marker.action !== 'none') {
                <div class="marker-icon">${iconHtml}</div>
                 div.addEventListener('click', function(e) {
                <div class="marker-tooltip">
                    <strong>${marker.name || 'Marcador'}</strong><br>
                    <small>${actionText}</small>
                </div>
                <div class="marker-badge ${!marker.hasBadge ? 'hidden' : ''}">${marker.hasBadge ? (marker.badgeNumber || marker.number || '') : ''}</div>
            `;
           
            // Hover effects
            $marker.addEventListener('mouseenter', () => {
                $marker.style.transform = 'translate(-50%, -50%) scale(1.15)';
            });
            $marker.addEventListener('mouseleave', () => {
                 $marker.style.transform = 'translate(-50%, -50%) scale(1)';
            });
           
            // Clique action
            if (hasAction) {
                $marker.addEventListener('click', (e) => {
                     e.stopPropagation();
                     e.stopPropagation();
                     this.executeAction(marker);
                     if (marker.action === 'popup') alert(marker.name + '\n\n' + (marker.actionData?.text || 'Sem informações'));
                    else if (marker.action === 'nextFloor') irProximoAndar();
                    else if (marker.action === 'prevFloor') irAndarAnterior();
                    else if (marker.action === 'gotoFloor' && marker.actionData?.floorId !== undefined) irParaAndar(marker.actionData.floorId);
                    else if (marker.action === 'link' && marker.actionData?.url && marker.actionData.url !== '#') window.open(marker.actionData.url, marker.actionData.target || '_blank');
                 });
                 });
             }
             }
           
             return div;
             return $marker;
         }
         }
          
          
         executeAction(marker) {
         function atualizarPosicaoMarcadores() {
             switch(marker.action) {
             for (var i = 0; i < marcadores.length; i++) {
                 case 'popup':
                 var item = marcadores[i];
                     this.showPopup(marker);
                if (parseInt(item.el.getAttribute('data-floor')) === andarAtual) {
                    break;
                     item.el.style.left = (item.data.x * zoomAtual) + 'px';
                case 'nextFloor':
                     item.el.style.top = (item.data.y * zoomAtual) + 'px';
                     this.nextFloor();
                     var ts = Math.max(20, Math.floor(20 * zoomAtual));
                    break;
                     var idiv = item.el.children[0];
                case 'prevFloor':
                     if (idiv) {
                     this.prevFloor();
                         if (item.data.iconBase64) idiv.innerHTML = '<img src="' + item.data.iconBase64 + '" style="width:' + ts + 'px; height:' + ts + 'px;">';
                     break;
                        else idiv.innerHTML = '<span style="font-size:' + ts + 'px;">📍</span>';
                case 'gotoFloor':
                     if (marker.actionData?.floorId !== undefined) {
                         this.goToFloor(marker.actionData.floorId);
                     }
                     }
                    break;
                 }
                 case 'link':
                    if (marker.actionData?.url && marker.actionData.url !== '#') {
                        window.open(marker.actionData.url, marker.actionData.target || '_blank');
                    }
                    break;
             }
             }
         }
         }
          
          
         initPopup() {
         function irProximoAndar() {
             if (document.querySelector('.mw-map-viewer-popup')) return;
             var idx = -1;
              
             for (var i = 0; i < config.layers.length; i++) {
            const popupHtml = `
                if (config.layers[i].id === andarAtual) idx = i;
                <div class="mw-map-viewer-popup" style="display:none;">
             }
                    <div class="popup-content">
             if (idx < config.layers.length - 1) irParaAndar(config.layers[idx + 1].id);
                        <span class="popup-close">&times;</span>
                        <div class="popup-body"></div>
                    </div>
                </div>
            `;
            document.body.insertAdjacentHTML('beforeend', popupHtml);
           
            this.popup = document.querySelector('.mw-map-viewer-popup');
            this.popupBody = this.popup?.querySelector('.popup-body');
              
             this.popup?.querySelector('.popup-close')?.addEventListener('click', () => this.closePopup());
            this.popup?.addEventListener('click', (e) => {
                if (e.target === this.popup) this.closePopup();
            });
         }
         }
          
          
         showPopup(marker) {
         function irAndarAnterior() {
             if (!this.popup) this.initPopup();
             var idx = -1;
           
            for (var i = 0; i < config.layers.length; i++) {
            const iconHtml = marker.iconBase64 ?
                 if (config.layers[i].id === andarAtual) idx = i;
                `<img src="${marker.iconBase64}" style="width:32px; height:32px; vertical-align:middle; margin-right:8px;">` :
            }
                `<span style="font-size:24px; margin-right:8px;">📍</span>`;
             if (idx > 0) irParaAndar(config.layers[idx - 1].id);
           
            this.popupBody.innerHTML = `
                 ${marker.actionData?.image ? `<img src="${marker.actionData.image}" alt="${marker.name}">` : ''}
                <div style="display:flex; align-items:center; margin-bottom:12px;">
                    ${iconHtml}
                    <h3 style="margin:0;">${marker.name}</h3>
                </div>
                <p>${marker.actionData?.text || 'Sem informações'}</p>
             `;
           
            this.popup.style.display = 'flex';
         }
         }
          
          
         closePopup() {
         function irParaAndar(floorId) {
             if (this.popup) this.popup.style.display = 'none';
             andarAtual = floorId;
            for (var i = 0; i < camadas.length; i++) {
                var lf = parseInt(camadas[i].getAttribute('data-floor'));
                camadas[i].style.display = lf === floorId ? 'block' : 'none';
            }
            var fd = null;
            for (var i = 0; i < config.layers.length; i++) {
                if (config.layers[i].id === floorId) fd = config.layers[i];
            }
            var nomeSpan = document.getElementById('floor-name-' + id);
            if (nomeSpan && fd) nomeSpan.textContent = fd.name;
            atualizarPosicaoMarcadores();
         }
         }
          
          
         updateMarkersPosition() {
         function zoomIn() {
             this.markers.forEach(({ element, data }) => {
             if (zoomAtual < zoomMax) {
                 if (parseInt(element.getAttribute('data-floor')) === this.currentFloor) {
                 var old = zoomAtual;
                    element.style.left = `${data.x * this.currentZoom}px`;
                zoomAtual = Math.min(zoomAtual + zoomPasso, zoomMax);
                    element.style.top = `${data.y * this.currentZoom}px`;
                aplicarZoom(old);
                   
             }
                    const iconSize = Math.max(20, Math.floor(20 * this.currentZoom));
                    const iconDiv = element.querySelector('.marker-icon');
                    if (iconDiv) {
                        if (data.iconBase64) {
                            iconDiv.innerHTML = `<img src="${data.iconBase64}" style="width:${iconSize}px; height:${iconSize}px;">`;
                        } else {
                            iconDiv.innerHTML = `<span style="font-size:${iconSize}px;">📍</span>`;
                        }
                    }
                }
             });
         }
         }
          
          
         zoomIn() {
         function zoomOut() {
             if (this.currentZoom < this.maxZoom) {
             if (zoomAtual > zoomMin) {
                 const oldZoom = this.currentZoom;
                 var old = zoomAtual;
                 this.currentZoom = Math.min(this.currentZoom + this.zoomStep, this.maxZoom);
                 zoomAtual = Math.max(zoomAtual - zoomPasso, zoomMin);
                 this.applyZoom(oldZoom);
                 aplicarZoom(old);
             }
             }
         }
         }
          
          
         zoomOut() {
         function aplicarZoom(oldZoom) {
             if (this.currentZoom > this.minZoom) {
             var imgs = document.querySelectorAll('.mapa-image');
                const oldZoom = this.currentZoom;
            for (var i = 0; i < imgs.length; i++) imgs[i].style.transform = 'scale(' + zoomAtual + ')';
                this.currentZoom = Math.max(this.currentZoom - this.zoomStep, this.minZoom);
            var rect = viewport.getBoundingClientRect();
                this.applyZoom(oldZoom);
            var cx = rect.width / 2, cy = rect.height / 2;
             }
            var sx = (viewport.scrollLeft + cx) / oldZoom, sy = (viewport.scrollTop + cy) / oldZoom;
            viewport.scrollLeft = sx * zoomAtual - cx;
            viewport.scrollTop = sy * zoomAtual - cy;
            atualizarPosicaoMarcadores();
            var zoomSpan = document.getElementById('zoom-level-' + id);
             if (zoomSpan) zoomSpan.textContent = Math.round(zoomAtual * 100) + '%';
         }
         }
          
          
         applyZoom(oldZoom) {
         function resetarView() {
             this.container.querySelectorAll('.map-image').forEach(img => {
             zoomAtual = config.mapConfig.defaultZoom || 1;
                img.style.transform = `scale(${this.currentZoom})`;
            var imgs = document.querySelectorAll('.mapa-image');
             });
             for (var i = 0; i < imgs.length; i++) imgs[i].style.transform = 'scale(' + zoomAtual + ')';
           
             viewport.scrollLeft = 0;
            const rect = this.viewport.getBoundingClientRect();
             viewport.scrollTop = 0;
            const cx = rect.width / 2, cy = rect.height / 2;
             atualizarPosicaoMarcadores();
            const sx = (this.viewport.scrollLeft + cx) / oldZoom;
             var zoomSpan = document.getElementById('zoom-level-' + id);
            const sy = (this.viewport.scrollTop + cy) / oldZoom;
             if (zoomSpan) zoomSpan.textContent = Math.round(zoomAtual * 100) + '%';
              
            this.viewport.scrollLeft = sx * this.currentZoom - cx;
             this.viewport.scrollTop = sy * this.currentZoom - cy;
              
             this.updateMarkersPosition();
             if (this.zoomLevelSpan) {
                this.zoomLevelSpan.textContent = `${Math.round(this.currentZoom * 100)}%`;
            }
         }
         }
          
          
         resetView() {
         // Conectar botões
            this.currentZoom = this.config.mapConfig?.defaultZoom || 1;
        document.getElementById('zoom-in-' + id).addEventListener('click', zoomIn);
            this.container.querySelectorAll('.map-image').forEach(img => {
        document.getElementById('zoom-out-' + id).addEventListener('click', zoomOut);
                img.style.transform = `scale(${this.currentZoom})`;
        document.getElementById('reset-' + id).addEventListener('click', resetarView);
             });
        document.getElementById('prev-' + id).addEventListener('click', irAndarAnterior);
             this.viewport.scrollLeft = 0;
        document.getElementById('next-' + id).addEventListener('click', irProximoAndar);
             this.viewport.scrollTop = 0;
       
             this.updateMarkersPosition();
        // Arrastar
             if (this.zoomLevelSpan) {
        viewport.addEventListener('mousedown', function(e) {
                this.zoomLevelSpan.textContent = `${Math.round(this.currentZoom * 100)}%`;
            if (e.target.closest('.mapa-marker')) return;
             }
             arrastando = true;
         }
             arrasteInicio = { x: e.clientX, y: e.clientY, scrollLeft: viewport.scrollLeft, scrollTop: viewport.scrollTop };
             viewport.style.cursor = 'grabbing';
             e.preventDefault();
        });
       
        window.addEventListener('mousemove', function(e) {
             if (!arrastando) return;
            viewport.scrollLeft = arrasteInicio.scrollLeft - (e.clientX - arrasteInicio.x);
             viewport.scrollTop = arrasteInicio.scrollTop - (e.clientY - arrasteInicio.y);
         });
          
          
         nextFloor() {
         window.addEventListener('mouseup', function() { arrastando = false; viewport.style.cursor = 'grab'; });
            const idx = this.config.layers.findIndex(l => l.id === this.currentFloor);
            if (idx < this.config.layers.length - 1) {
                this.goToFloor(this.config.layers[idx + 1].id);
            }
        }
          
          
         prevFloor() {
         viewport.addEventListener('wheel', function(e) {
             const idx = this.config.layers.findIndex(l => l.id === this.currentFloor);
             if (e.ctrlKey) { e.preventDefault(); e.deltaY < 0 ? zoomIn() : zoomOut(); }
            if (idx > 0) {
         });
                this.goToFloor(this.config.layers[idx - 1].id);
            }
         }
          
          
         goToFloor(floorId) {
         viewport.addEventListener('mousemove', function(e) {
             this.currentFloor = floorId;
             var rect = viewport.getBoundingClientRect();
              
             var x = (e.clientX - rect.left + viewport.scrollLeft) / zoomAtual;
            this.layers.forEach(layer => {
             var y = (e.clientY - rect.top + viewport.scrollTop) / zoomAtual;
                const layerFloor = parseInt(layer.getAttribute('data-floor'));
             coordsDiv.textContent = '📍 ' + Math.round(x) + ', ' + Math.round(y) + ' | ' + Math.round(zoomAtual * 100) + '%';
                layer.style.display = layerFloor === floorId ? 'block' : 'none';
         });
            });
              
            const floorData = this.config.layers.find(l => l.id === floorId);
             if (floorData && this.floorNameSpan) {
                this.floorNameSpan.textContent = floorData.name;
            }
           
            setTimeout(() => this.updateMarkersPosition(), 50);
         }
          
          
         // Método para atualizar configuração dinamicamente
         renderizarCamadas();
        updateConfig(config) {
            this.config = this.normalizeConfig(config);
            this.renderLayers();
            this.updateMarkersPosition();
        }
     }
     }
   
    // Registrar widget globalmente
    window.MWMapViewer = MWMapViewer;
    window.MWMapViewerStyles = styles;
})();
})();
</script></includeonly>

Edição atual tal como às 20h38min de 9 de abril de 2026