Mudanças entre as edições de "Módulo:MapaJson"

De Wiki Gla
Ir para navegação Ir para pesquisar
Linha 1: Linha 1:
-- Módulo:MapaJson
-- Processa JSON para o visualizador de mapas
local p = {}
local p = {}


-- Função para escapar string para JavaScript
function p.renderizar(frame)
function p.escapeJS(str)
     -- Pegar os parâmetros
    if not str then return '' end
    str = str:gsub("\\", "\\\\")
    str = str:gsub("'", "\\'")
    str = str:gsub('"', '\\"')
    str = str:gsub("\n", "\\n")
    str = str:gsub("\r", "\\r")
    str = str:gsub("\t", "\\t")
     return str
end
 
-- Função para minificar JSON
function p.minifyJSON(json)
    if not json then return '' end
    json = json:gsub("\n", "")
    json = json:gsub("\r", "")
    json = json:gsub("\t", " ")
    json = json:gsub("  +", " ")
    json = json:gsub("%s*({)%s*", "%1")
    json = json:gsub("%s*(})%s*", "%1")
    json = json:gsub("%s*(%[)%s*", "%1")
    json = json:gsub("%s*(%])%s*", "%1")
    json = json:gsub("%s*(:)%s*", "%1")
    json = json:gsub("%s*(,)%s*", "%1")
    return json
end
 
-- Função principal
function p.render(frame)
     local args = frame.args
     local args = frame.args
     local jsonString = args.json or ''
     local mapaId = args.id or 'mapa1'
     local mapId = args.id or 'mapa1'
     local largura = args.largura or '100%'
     local width = args.largura or '100%'
     local altura = args.altura or '500px'
     local height = args.altura or '500px'
     local titulo = args.titulo or 'Mapa'
     local mapName = args.nome or 'Mapa'
     local jsonFonte = args.json or ''
      
      
     -- Minificar e escapar
     -- Se o JSON veio de uma página (ex: {{:Mapa:Config}}), buscar o conteúdo
     local minifiedJSON = p.minifyJSON(jsonString)
     if jsonFonte:match("^%{%{:[^}]+%}%}$") then
    local escapedJSON = p.escapeJS(minifiedJSON)
        local nomePagina = jsonFonte:match("^%{%{:([^}]+)%}%}$")
        if nomePagina then
            local title = mw.title.new(nomePagina)
            if title and title.exists then
                jsonFonte = title:getContent() or ''
            end
        end
    end
      
      
     -- Gerar HTML/JS
    -- Limpar o JSON (remover quebras de linha e espaços extras)
     local output = [[
    jsonFonte = jsonFonte:gsub("\n", "")
<div id="mapa-viewer-]] .. mapId .. [[" style="width:]] .. width .. [[; height:]] .. height .. [[; background:#0f172a; border-radius:12px; overflow:hidden; position:relative;">
    jsonFonte = jsonFonte:gsub("\r", "")
    jsonFonte = jsonFonte:gsub("\t", " ")
    jsonFonte = jsonFonte:gsub("  +", " ")
   
    -- Escapar para usar dentro de string JavaScript
    local jsonEscapado = jsonFonte:gsub("'", "\\'")
   
     -- Gerar o HTML com o visualizador
     local html = [[
<div id="mapa-container-]] .. mapaId .. [[" style="width:]] .. largura .. [[; height:]] .. altura .. [[; background:#0f172a; border-radius:12px; overflow:hidden; position:relative;">
     <div style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); color:#64748b; text-align:center;">
     <div style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); color:#64748b; text-align:center;">
         <div>🗺️ Carregando mapa...</div>
         🗺️ Carregando mapa...<br>
         <div style="font-size:12px; margin-top:8px;">]] .. mapName .. [[</div>
         <small>]] .. titulo .. [[</small>
     </div>
     </div>
</div>
</div>
<script>
<script>
(function() {
(function() {
     var containerId = 'mapa-viewer-]] .. mapId .. [[';
     var containerId = 'mapa-container-]] .. mapaId .. [[';
     var container = document.getElementById(containerId);
     var container = document.getElementById(containerId);
     if (!container) return;
     if (!container) return;
     var jsonString = ']] .. escapedJSON .. [[';
   
    if (!jsonString || jsonString === '') {
     var jsonString = ']] .. jsonEscapado .. [[';
        container.innerHTML = '<div style="padding:20px; text-align:center; color:#ef4444;">❌ Configuração não encontrada</div>';
 
        return;
]] .. p.obterCodigoJS() .. [[
    }
 
    var mapConfig;
     try {
     try {
         mapConfig = JSON.parse(jsonString);
         var config = JSON.parse(jsonString);
        if (config.layers && config.layers.length > 0) {
            carregarMapa(containerId, config);
        } else {
            container.innerHTML = '<div style="padding:20px; text-align:center; color:#f59e0b;">⚠️ Nenhuma camada encontrada</div>';
        }
     } catch(e) {
     } catch(e) {
         container.innerHTML = '<div style="padding:20px; text-align:center; color:#ef4444;">❌ Erro no JSON: ' + e.message + '</div>';
         container.innerHTML = '<div style="padding:20px; text-align:center; color:#ef4444;">❌ Erro ao carregar mapa: ' + e.message + '</div>';
        return;
     }
     }
     if (!mapConfig.layers || mapConfig.layers.length === 0) {
})();
        container.innerHTML = '<div style="padding:20px; text-align:center; color:#f59e0b;">⚠️ Nenhuma camada configurada</div>';
</script>]]
         return;
   
     }
    return html
     iniciarMapaViewer(containerId, mapConfig);
end
 
function p.obterCodigoJS()
    return [[
function carregarMapa(containerId, config) {
    var container = document.getElementById(containerId);
     if (!container) return;
   
    container.innerHTML = '';
    container.style.position = 'relative';
    container.style.overflow = 'hidden';
   
    // Zoom e navegação
    var zoomAtual = config.mapConfig.defaultZoom || 1;
    var andarAtual = config.mapConfig.initialFloor || 0;
    var zoomMin = config.mapConfig.minZoom || 0.5;
    var zoomMax = config.mapConfig.maxZoom || 3;
    var zoomPasso = config.mapConfig.zoomStep || 0.1;
   
    var camadas = [];
    var marcadores = [];
    var arrastando = false;
    var arrasteInicio = { x: 0, y: 0, scrollLeft: 0, scrollTop: 0 };
   
    // Criar toolbar
    var toolbar = document.createElement('div');
    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;';
    toolbar.innerHTML = '<div style="display:flex; gap:5px; background:rgba(0,0,0,0.7); padding:5px 10px; border-radius:30px;">' +
        '<button id="btn-zoom-in-' + containerId + '" style="background:#334155; border:none; color:white; width:32px; height:32px; border-radius:50%; cursor:pointer;">+</button>' +
        '<button id="btn-zoom-out-' + containerId + '" style="background:#334155; border:none; color:white; width:32px; height:32px; border-radius:50%; cursor:pointer;">-</button>' +
        '<button id="btn-reset-' + containerId + '" style="background:#334155; border:none; color:white; width:32px; height:32px; border-radius:50%; cursor:pointer;">⟳</button>' +
        '</div>' +
        '<div style="background:rgba(0,0,0,0.7); padding:5px 15px; border-radius:30px; color:white; font-size:12px;"><span id="floor-name-' + containerId + '">' + (config.layers[0]?.name || 'Mapa') + '</span></div>' +
         '<div id="zoom-level-' + containerId + '" style="background:rgba(0,0,0,0.7); padding:5px 12px; border-radius:30px; color:#a5b4fc; font-size:12px;">100%</div>';
   
    // Viewport
    var viewport = document.createElement('div');
    viewport.style.cssText = 'width:100%; height:100%; overflow:auto; cursor:grab; background:#0f172a;';
    var camadasDiv = document.createElement('div');
    camadasDiv.style.cssText = 'position:relative; min-width:100%; min-height:100%;';
    viewport.appendChild(camadasDiv);
      
     // Navegação inferior
    var navDiv = document.createElement('div');
    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;';
    navDiv.innerHTML = '<button id="btn-prev-' + containerId + '" style="background:#334155; border:none; color:white; width:36px; height:36px; border-radius:50%; cursor:pointer;">▲</button>' +
        '<button id="btn-next-' + containerId + '" style="background:#334155; border:none; color:white; width:36px; height:36px; border-radius:50%; cursor:pointer;">▼</button>';
   
    // Coordenadas
    var coordsDiv = document.createElement('div');
    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;';
    coordsDiv.textContent = '📍 0, 0';
   
    container.appendChild(toolbar);
    container.appendChild(viewport);
    container.appendChild(coordsDiv);
    container.appendChild(navDiv);
      
      
     function iniciarMapaViewer(containerId, config) {
    // Renderizar camadas
         var container = document.getElementById(containerId);
     function renderizarCamadas() {
        if (!container) return;
         camadasDiv.innerHTML = '';
        container.innerHTML = '';
         camadas = [];
         container.style.position = 'relative';
         marcadores = [];
         container.style.overflow = 'hidden';
          
          
         var toolbar = document.createElement('div');
         if (!config.layers || config.layers.length === 0) return;
        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;';
        toolbar.innerHTML = '<div style="display:flex; gap:5px; background:rgba(0,0,0,0.7); padding:5px 10px; border-radius:30px;"><button class="mapa-zoom-in" style="background:#334155; border:none; color:white; width:32px; height:32px; border-radius:50%; cursor:pointer;">+</button><button class="mapa-zoom-out" style="background:#334155; border:none; color:white; width:32px; height:32px; border-radius:50%; cursor:pointer;">-</button><button class="mapa-reset" style="background:#334155; border:none; color:white; width:32px; height:32px; border-radius:50%; cursor:pointer;">⟳</button></div><div style="background:rgba(0,0,0,0.7); padding:5px 15px; border-radius:30px; color:white; font-size:12px;"><span class="mapa-floor-name">' + (config.layers[0]?.name || 'Mapa') + '</span></div><div style="background:rgba(0,0,0,0.7); padding:5px 12px; border-radius:30px; color:#a5b4fc; font-size:12px;" class="mapa-zoom-level">100%</div>';
          
          
         var viewport = document.createElement('div');
         var andarExiste = false;
         viewport.className = 'mapa-viewport';
         for (var i = 0; i < config.layers.length; i++) {
        viewport.style.cssText = 'width:100%; height:100%; overflow:auto; cursor:grab; background:#0f172a;';
            if (config.layers[i].id === andarAtual) andarExiste = true;
        var layersContainer = document.createElement('div');
         }
         layersContainer.className = 'mapa-layers';
         if (!andarExiste && config.layers.length > 0) andarAtual = config.layers[0].id;
         layersContainer.style.cssText = 'position:relative; min-width:100%; min-height:100%;';
        viewport.appendChild(layersContainer);
          
          
         var coordsDiv = document.createElement('div');
         var andarInfo = null;
         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;';
         for (var i = 0; i < config.layers.length; i++) {
         coordsDiv.textContent = '📍 0, 0';
            if (config.layers[i].id === andarAtual) andarInfo = config.layers[i];
        }
        var nomeSpan = document.getElementById('floor-name-' + containerId);
         if (nomeSpan && andarInfo) nomeSpan.textContent = andarInfo.name;
          
          
         var navDiv = document.createElement('div');
         for (var i = 0; i < config.layers.length; i++) {
        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;';
            var layer = config.layers[i];
        navDiv.innerHTML = '<button class="mapa-prev-floor" style="background:#334155; border:none; color:white; width:36px; height:36px; border-radius:50%; cursor:pointer;">▲</button><button class="mapa-next-floor" style="background:#334155; border:none; color:white; width:36px; height:36px; border-radius:50%; cursor:pointer;">▼</button>';
            var divLayer = document.createElement('div');
       
            divLayer.className = 'mapa-layer';
        container.appendChild(toolbar);
            divLayer.setAttribute('data-floor', layer.id);
        container.appendChild(viewport);
            divLayer.style.display = layer.id === andarAtual ? 'block' : 'none';
        container.appendChild(coordsDiv);
            divLayer.style.position = 'absolute';
        container.appendChild(navDiv);
            divLayer.style.top = '0';
       
            divLayer.style.left = '0';
        var currentZoom = config.mapConfig.defaultZoom || 1;
            divLayer.style.transform = 'translate(' + (layer.alignment?.offsetX || 0) + 'px, ' + (layer.alignment?.offsetY || 0) + 'px)';
        var currentFloor = config.mapConfig.initialFloor || 0;
            divLayer.style.opacity = (layer.opacity || 100) / 100;
        var minZoom = config.mapConfig.minZoom || 0.5;
           
        var maxZoom = config.mapConfig.maxZoom || 3;
            var img = document.createElement('img');
        var zoomStep = config.mapConfig.zoomStep || 0.1;
            img.style.display = 'block';
        var layers = [];
            img.style.transform = 'scale(' + zoomAtual + ')';
        var markers = [];
            img.style.transformOrigin = '0 0';
        var isPanning = false;
            img.src = layer.imagePath || '';
        var panStart = { x: 0, y: 0, scrollLeft: 0, scrollTop: 0 };
            img.onload = function() { camadasDiv.style.width = this.width + 'px'; camadasDiv.style.height = this.height + 'px'; };
        var floorNameSpan = toolbar.querySelector('.mapa-floor-name');
            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'; };
        var zoomLevelSpan = toolbar.querySelector('.mapa-zoom-level');
             divLayer.appendChild(img);
       
              
        function renderizarCamadas() {
             var marcadoresDiv = document.createElement('div');
            layersContainer.innerHTML = '';
            marcadoresDiv.style.position = 'absolute';
            layers = [];
             marcadoresDiv.style.top = '0';
             markers = [];
             marcadoresDiv.style.left = '0';
             var floorExists = false;
             marcadoresDiv.style.width = '100%';
             for (var i = 0; i < config.layers.length; i++) {
            marcadoresDiv.style.height = '100%';
                if (config.layers[i].id === currentFloor) floorExists = true;
             marcadoresDiv.style.pointerEvents = 'none';
             }
            if (!floorExists && config.layers.length > 0) currentFloor = config.layers[0].id;
             var floorAtual = null;
             for (var i = 0; i < config.layers.length; i++) {
                if (config.layers[i].id === currentFloor) floorAtual = config.layers[i];
             }
            if (floorAtual && floorNameSpan) floorNameSpan.textContent = floorAtual.name;
              
              
             for (var i = 0; i < config.layers.length; i++) {
             if (layer.markers) {
                var layer = config.layers[i];
                for (var j = 0; j < layer.markers.length; j++) {
                var divLayer = document.createElement('div');
                    var marker = layer.markers[j];
                divLayer.className = 'mapa-layer';
                    var markerDiv = criarMarcador(marker, layer.id);
                divLayer.setAttribute('data-floor', layer.id);
                    marcadoresDiv.appendChild(markerDiv);
                divLayer.style.display = layer.id === currentFloor ? 'block' : 'none';
                    marcadores.push({ el: markerDiv, data: marker });
                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;
               
                var img = document.createElement('img');
                img.style.display = 'block';
                img.style.transform = 'scale(' + currentZoom + ')';
                img.style.transformOrigin = '0 0';
                img.src = layer.imagePath || '';
                img.onload = function() { layersContainer.style.width = this.width + 'px'; layersContainer.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%3E%3C!%5BCDATA[Sem imagem%5D%5D%3E%3C/text%3E%3C/svg%3E'; };
                divLayer.appendChild(img);
               
                var markersDiv = document.createElement('div');
                markersDiv.style.position = 'absolute';
                markersDiv.style.top = '0';
                markersDiv.style.left = '0';
                markersDiv.style.width = '100%';
                markersDiv.style.height = '100%';
                markersDiv.style.pointerEvents = 'none';
               
                if (layer.markers) {
                    for (var j = 0; j < layer.markers.length; j++) {
                        var marker = layer.markers[j];
                        var markerDiv = criarMarcador(marker, layer.id);
                        markersDiv.appendChild(markerDiv);
                        markers.push({ el: markerDiv, data: marker });
                    }
                 }
                 }
                divLayer.appendChild(markersDiv);
                layersContainer.appendChild(divLayer);
                layers.push(divLayer);
             }
             }
           
            divLayer.appendChild(marcadoresDiv);
            camadasDiv.appendChild(divLayer);
            camadas.push(divLayer);
         }
         }
    }
   
    function criarMarcador(marker, floorId) {
        var div = document.createElement('div');
        div.className = 'mapa-marker';
        div.setAttribute('data-marker-id', marker.id);
        div.setAttribute('data-floor', floorId);
        div.setAttribute('data-x', marker.x);
        div.setAttribute('data-y', marker.y);
          
          
         function criarMarcador(marker, floorId) {
         div.style.position = 'absolute';
            var div = document.createElement('div');
        div.style.left = (marker.x * zoomAtual) + 'px';
            div.className = 'mapa-marker';
        div.style.top = (marker.y * zoomAtual) + 'px';
            div.setAttribute('data-marker-id', marker.id);
        div.style.zIndex = '100';
            div.setAttribute('data-floor', floorId);
        div.style.transform = 'translate(-50%, -50%)';
            div.setAttribute('data-x', marker.x);
        div.style.cursor = (marker.action && marker.action !== 'none') ? 'pointer' : 'default';
            div.setAttribute('data-y', marker.y);
       
            div.style.position = 'absolute';
        var tamanhoIcone = Math.max(20, Math.floor(20 * zoomAtual));
            div.style.left = (marker.x * currentZoom) + 'px';
        var htmlIcone = marker.iconBase64 ?  
            div.style.top = (marker.y * currentZoom) + 'px';
            '<img src="' + marker.iconBase64 + '" style="width:' + tamanhoIcone + 'px; height:' + tamanhoIcone + 'px;">' :  
            div.style.zIndex = '100';
            '<span style="font-size:' + tamanhoIcone + 'px;">📍</span>';
            div.style.transform = 'translate(-50%, -50%)';
       
            div.style.cursor = (marker.action && marker.action !== 'none') ? 'pointer' : 'default';
        var textoAcao = '';
            var tamanhoIcone = Math.max(20, Math.floor(20 * currentZoom));
        if (marker.action === 'popup') textoAcao = '📋 Informações';
            var htmlIcone = marker.iconBase64 ? '<img src="' + marker.iconBase64 + '" style="width:' + tamanhoIcone + 'px; height:' + tamanhoIcone + 'px;">' : '<span style="font-size:' + tamanhoIcone + 'px;">📍</span>';
        else if (marker.action === 'nextFloor') textoAcao = '⬆️ Próximo andar';
            var textoAcao = '';
        else if (marker.action === 'prevFloor') textoAcao = '⬇️ Andar anterior';
            if (marker.action === 'popup') textoAcao = '📋 Informações';
        else if (marker.action === 'gotoFloor') textoAcao = '🎯 Ir para andar';
            else if (marker.action === 'nextFloor') textoAcao = '⬆️ Próximo andar';
        else if (marker.action === 'link') textoAcao = '🔗 Link externo';
            else if (marker.action === 'prevFloor') textoAcao = '⬇️ Andar anterior';
        else textoAcao = '📍 Clique';
            else if (marker.action === 'gotoFloor') textoAcao = '🎯 Ir para andar';
            else if (marker.action === 'link') textoAcao = '🔗 Link externo';
            else textoAcao = '📍 Clique';
           
            div.innerHTML = '<div style="display:flex; align-items:center; justify-content:center;">' + htmlIcone + '</div><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;"><strong>' + (marker.name || 'Marcador') + '</strong><br><small>' + textoAcao + '</small></div><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>';
           
            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'; } });
            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'; } });
            if (marker.action && marker.action !== 'none') {
                div.addEventListener('click', function(e) { e.stopPropagation(); executarAcao(marker); });
            }
            return div;
        }
          
          
         function executarAcao(marker) {
         div.innerHTML = '<div style="display:flex; align-items:center; justify-content:center;">' + htmlIcone + '</div>' +
            if (marker.action === 'popup') alert(marker.name + '\n\n' + (marker.actionData?.text || 'Sem informações'));
            '<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;">' +
            else if (marker.action === 'nextFloor') proximoAndar();
            '<strong>' + (marker.name || 'Marcador') + '</strong><br><small>' + textoAcao + '</small></div>' +
            else if (marker.action === 'prevFloor') andarAnterior();
             '<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>';
             else if (marker.action === 'gotoFloor') { if (marker.actionData?.floorId !== undefined) irParaAndar(marker.actionData.floorId); }
            else if (marker.action === 'link') { if (marker.actionData?.url && marker.actionData.url !== '#') window.open(marker.actionData.url, marker.actionData.target || '_blank'); }
        }
          
          
         function proximoAndar() { var idx = -1; for (var i = 0; i < config.layers.length; i++) { if (config.layers[i].id === currentFloor) idx = i; } if (idx < config.layers.length - 1) irParaAndar(config.layers[idx + 1].id); }
         div.addEventListener('mouseenter', function() {
         function andarAnterior() { var idx = -1; for (var i = 0; i < config.layers.length; i++) { if (config.layers[i].id === currentFloor) idx = i; } if (idx > 0) irParaAndar(config.layers[idx - 1].id); }
            this.style.transform = 'translate(-50%, -50%) scale(1.15)';
            var tp = this.children[1];
            if (tp) { tp.style.opacity = '1'; tp.style.visibility = 'visible'; }
        });
         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'; }
        });
          
          
         function irParaAndar(floorId) {
         if (marker.action && marker.action !== 'none') {
            currentFloor = floorId;
            div.addEventListener('click', function(e) {
            for (var i = 0; i < layers.length; i++) { var lf = parseInt(layers[i].getAttribute('data-floor')); layers[i].style.display = lf === floorId ? 'block' : 'none'; }
                e.stopPropagation();
            var fd = null; for (var i = 0; i < config.layers.length; i++) { if (config.layers[i].id === floorId) fd = config.layers[i]; }
                if (marker.action === 'popup') alert(marker.name + '\n\n' + (marker.actionData?.text || 'Sem informações'));
            if (fd && floorNameSpan) floorNameSpan.textContent = fd.name;
                else if (marker.action === 'nextFloor') irProximoAndar();
             atualizarPosicaoMarkers();
                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;
        function atualizarPosicaoMarkers() {
    }
            for (var i = 0; i < markers.length; i++) {
   
                var item = markers[i];
    function atualizarPosicaoMarcadores() {
                if (parseInt(item.el.getAttribute('data-floor')) === currentFloor) {
        for (var i = 0; i < marcadores.length; i++) {
                    item.el.style.left = (item.data.x * currentZoom) + 'px';
            var item = marcadores[i];
                    item.el.style.top = (item.data.y * currentZoom) + 'px';
            if (parseInt(item.el.getAttribute('data-floor')) === andarAtual) {
                    var ts = Math.max(20, Math.floor(20 * currentZoom));
                item.el.style.left = (item.data.x * zoomAtual) + 'px';
                    var idiv = item.el.children[0];
                item.el.style.top = (item.data.y * zoomAtual) + 'px';
                    if (idiv) {
                var ts = Math.max(20, Math.floor(20 * zoomAtual));
                        if (item.data.iconBase64) idiv.innerHTML = '<img src="' + item.data.iconBase64 + '" style="width:' + ts + 'px; height:' + ts + 'px;">';
                var idiv = item.el.children[0];
                        else idiv.innerHTML = '<span style="font-size:' + ts + 'px;">📍</span>';
                if (idiv) {
                    }
                    if (item.data.iconBase64) idiv.innerHTML = '<img src="' + item.data.iconBase64 + '" style="width:' + ts + 'px; height:' + ts + 'px;">';
                    else idiv.innerHTML = '<span style="font-size:' + ts + 'px;">📍</span>';
                 }
                 }
             }
             }
         }
         }
          
    }
         function zoomIn() { if (currentZoom < maxZoom) { var old = currentZoom; currentZoom = Math.min(currentZoom + zoomStep, maxZoom); aplicarZoom(old); } }
   
         function zoomOut() { if (currentZoom > minZoom) { var old = currentZoom; currentZoom = Math.max(currentZoom - zoomStep, minZoom); aplicarZoom(old); } }
    function irProximoAndar() {
       
         var idx = -1;
        function aplicarZoom(oldZoom) {
         for (var i = 0; i < config.layers.length; i++) {
            var imgs = document.querySelectorAll('.mapa-image');
            if (config.layers[i].id === andarAtual) idx = i;
            for (var i = 0; i < imgs.length; i++) imgs[i].style.transform = 'scale(' + currentZoom + ')';
        }
            var rect = viewport.getBoundingClientRect();
        if (idx < config.layers.length - 1) irParaAndar(config.layers[idx + 1].id);
            var cx = rect.width / 2, cy = rect.height / 2;
    }
             var sx = (viewport.scrollLeft + cx) / oldZoom, sy = (viewport.scrollTop + cy) / oldZoom;
   
            viewport.scrollLeft = sx * currentZoom - cx;
    function irAndarAnterior() {
            viewport.scrollTop = sy * currentZoom - cy;
        var idx = -1;
            atualizarPosicaoMarkers();
         for (var i = 0; i < config.layers.length; i++) {
            if (zoomLevelSpan) zoomLevelSpan.textContent = Math.round(currentZoom * 100) + '%';
            if (config.layers[i].id === andarAtual) idx = i;
        }
        if (idx > 0) irParaAndar(config.layers[idx - 1].id);
    }
   
    function irParaAndar(floorId) {
        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-' + containerId);
        if (nomeSpan && fd) nomeSpan.textContent = fd.name;
        atualizarPosicaoMarcadores();
    }
   
    function zoomIn() {
        if (zoomAtual < zoomMax) {
            var old = zoomAtual;
            zoomAtual = Math.min(zoomAtual + zoomPasso, zoomMax);
            aplicarZoom(old);
         }
         }
       
    }
        function resetarView() {
   
            currentZoom = config.mapConfig.defaultZoom || 1;
    function zoomOut() {
            var imgs = document.querySelectorAll('.mapa-image');
        if (zoomAtual > zoomMin) {
             for (var i = 0; i < imgs.length; i++) imgs[i].style.transform = 'scale(' + currentZoom + ')';
             var old = zoomAtual;
             viewport.scrollLeft = 0;
             zoomAtual = Math.max(zoomAtual - zoomPasso, zoomMin);
            viewport.scrollTop = 0;
             aplicarZoom(old);
            atualizarPosicaoMarkers();
             if (zoomLevelSpan) zoomLevelSpan.textContent = Math.round(currentZoom * 100) + '%';
         }
         }
       
        var btnZoomIn = toolbar.querySelector('.mapa-zoom-in');
        var btnZoomOut = toolbar.querySelector('.mapa-zoom-out');
        var btnReset = toolbar.querySelector('.mapa-reset');
        var btnPrev = navDiv.querySelector('.mapa-prev-floor');
        var btnNext = navDiv.querySelector('.mapa-next-floor');
        if (btnZoomIn) btnZoomIn.addEventListener('click', zoomIn);
        if (btnZoomOut) btnZoomOut.addEventListener('click', zoomOut);
        if (btnReset) btnReset.addEventListener('click', resetarView);
        if (btnPrev) btnPrev.addEventListener('click', andarAnterior);
        if (btnNext) btnNext.addEventListener('click', proximoAndar);
       
        viewport.addEventListener('mousedown', function(e) { if (e.target.closest('.mapa-marker')) return; isPanning = true; panStart = { x: e.clientX, y: e.clientY, scrollLeft: viewport.scrollLeft, scrollTop: viewport.scrollTop }; viewport.style.cursor = 'grabbing'; e.preventDefault(); });
        window.addEventListener('mousemove', function(e) { if (!isPanning) return; viewport.scrollLeft = panStart.scrollLeft - (e.clientX - panStart.x); viewport.scrollTop = panStart.scrollTop - (e.clientY - panStart.y); });
        window.addEventListener('mouseup', function() { isPanning = false; viewport.style.cursor = 'grab'; });
        viewport.addEventListener('wheel', function(e) { if (e.ctrlKey) { e.preventDefault(); e.deltaY < 0 ? zoomIn() : zoomOut(); } });
        viewport.addEventListener('mousemove', function(e) { var rect = viewport.getBoundingClientRect(); var x = (e.clientX - rect.left + viewport.scrollLeft) / currentZoom; var y = (e.clientY - rect.top + viewport.scrollTop) / currentZoom; coordsDiv.textContent = '📍 ' + Math.round(x) + ', ' + Math.round(y) + ' | ' + Math.round(currentZoom * 100) + '%'; });
       
        renderizarCamadas();
     }
     }
})();
</script>]]
      
      
     -- Usar frame:extensionTag para garantir que o HTML seja processado corretamente
     function aplicarZoom(oldZoom) {
     return output
        var imgs = document.querySelectorAll('.mapa-image');
        for (var i = 0; i < imgs.length; i++) imgs[i].style.transform = 'scale(' + zoomAtual + ')';
        var rect = viewport.getBoundingClientRect();
        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-' + containerId);
        if (zoomSpan) zoomSpan.textContent = Math.round(zoomAtual * 100) + '%';
    }
   
    function resetarView() {
        zoomAtual = config.mapConfig.defaultZoom || 1;
        var imgs = document.querySelectorAll('.mapa-image');
        for (var i = 0; i < imgs.length; i++) imgs[i].style.transform = 'scale(' + zoomAtual + ')';
        viewport.scrollLeft = 0;
        viewport.scrollTop = 0;
        atualizarPosicaoMarcadores();
        var zoomSpan = document.getElementById('zoom-level-' + containerId);
        if (zoomSpan) zoomSpan.textContent = Math.round(zoomAtual * 100) + '%';
    }
   
    // Eventos dos botões
    document.getElementById('btn-zoom-in-' + containerId).addEventListener('click', zoomIn);
    document.getElementById('btn-zoom-out-' + containerId).addEventListener('click', zoomOut);
    document.getElementById('btn-reset-' + containerId).addEventListener('click', resetarView);
    document.getElementById('btn-prev-' + containerId).addEventListener('click', irAndarAnterior);
    document.getElementById('btn-next-' + containerId).addEventListener('click', irProximoAndar);
   
    // Arrastar mapa
    viewport.addEventListener('mousedown', function(e) {
        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);
    });
   
    window.addEventListener('mouseup', function() { arrastando = false; viewport.style.cursor = 'grab'; });
   
    // Zoom com scroll
    viewport.addEventListener('wheel', function(e) {
        if (e.ctrlKey) { e.preventDefault(); e.deltaY < 0 ? zoomIn() : zoomOut(); }
    });
   
    // Coordenadas
    viewport.addEventListener('mousemove', function(e) {
        var rect = viewport.getBoundingClientRect();
        var x = (e.clientX - rect.left + viewport.scrollLeft) / zoomAtual;
        var y = (e.clientY - rect.top + viewport.scrollTop) / zoomAtual;
        coordsDiv.textContent = '📍 ' + Math.round(x) + ', ' + Math.round(y) + ' | ' + Math.round(zoomAtual * 100) + '%';
    });
   
    renderizarCamadas();
}
]]
end
end


return p
return p

Edição das 19h48min de 9 de abril de 2026

A documentação para este módulo pode ser criada em Módulo:MapaJson/doc

local p = {}

function p.renderizar(frame)
    -- Pegar os parâmetros
    local args = frame.args
    local mapaId = args.id or 'mapa1'
    local largura = args.largura or '100%'
    local altura = args.altura or '500px'
    local titulo = args.titulo or 'Mapa'
    local jsonFonte = args.json or ''
    
    -- Se o JSON veio de uma página (ex: {{:Mapa:Config}}), buscar o conteúdo
    if jsonFonte:match("^%{%{:[^}]+%}%}$") then
        local nomePagina = jsonFonte:match("^%{%{:([^}]+)%}%}$")
        if nomePagina then
            local title = mw.title.new(nomePagina)
            if title and title.exists then
                jsonFonte = title:getContent() or ''
            end
        end
    end
    
    -- Limpar o JSON (remover quebras de linha e espaços extras)
    jsonFonte = jsonFonte:gsub("\n", "")
    jsonFonte = jsonFonte:gsub("\r", "")
    jsonFonte = jsonFonte:gsub("\t", " ")
    jsonFonte = jsonFonte:gsub("  +", " ")
    
    -- Escapar para usar dentro de string JavaScript
    local jsonEscapado = jsonFonte:gsub("'", "\\'")
    
    -- Gerar o HTML com o visualizador
    local html = [[
<div id="mapa-container-]] .. mapaId .. [[" style="width:]] .. largura .. [[; height:]] .. altura .. [[; background:#0f172a; border-radius:12px; overflow:hidden; position:relative;">
    <div style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); color:#64748b; text-align:center;">
        🗺️ Carregando mapa...<br>
        <small>]] .. titulo .. [[</small>
    </div>
</div>

<script>
(function() {
    var containerId = 'mapa-container-]] .. mapaId .. [[';
    var container = document.getElementById(containerId);
    if (!container) return;
    
    var jsonString = ']] .. jsonEscapado .. [[';

]] .. p.obterCodigoJS() .. [[

    try {
        var config = JSON.parse(jsonString);
        if (config.layers && config.layers.length > 0) {
            carregarMapa(containerId, config);
        } else {
            container.innerHTML = '<div style="padding:20px; text-align:center; color:#f59e0b;">⚠️ Nenhuma camada encontrada</div>';
        }
    } catch(e) {
        container.innerHTML = '<div style="padding:20px; text-align:center; color:#ef4444;">❌ Erro ao carregar mapa: ' + e.message + '</div>';
    }
})();
</script>]]
    
    return html
end

function p.obterCodigoJS()
    return [[
function carregarMapa(containerId, config) {
    var container = document.getElementById(containerId);
    if (!container) return;
    
    container.innerHTML = '';
    container.style.position = 'relative';
    container.style.overflow = 'hidden';
    
    // Zoom e navegação
    var zoomAtual = config.mapConfig.defaultZoom || 1;
    var andarAtual = config.mapConfig.initialFloor || 0;
    var zoomMin = config.mapConfig.minZoom || 0.5;
    var zoomMax = config.mapConfig.maxZoom || 3;
    var zoomPasso = config.mapConfig.zoomStep || 0.1;
    
    var camadas = [];
    var marcadores = [];
    var arrastando = false;
    var arrasteInicio = { x: 0, y: 0, scrollLeft: 0, scrollTop: 0 };
    
    // Criar toolbar
    var toolbar = document.createElement('div');
    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;';
    toolbar.innerHTML = '<div style="display:flex; gap:5px; background:rgba(0,0,0,0.7); padding:5px 10px; border-radius:30px;">' +
        '<button id="btn-zoom-in-' + containerId + '" style="background:#334155; border:none; color:white; width:32px; height:32px; border-radius:50%; cursor:pointer;">+</button>' +
        '<button id="btn-zoom-out-' + containerId + '" style="background:#334155; border:none; color:white; width:32px; height:32px; border-radius:50%; cursor:pointer;">-</button>' +
        '<button id="btn-reset-' + containerId + '" style="background:#334155; border:none; color:white; width:32px; height:32px; border-radius:50%; cursor:pointer;">⟳</button>' +
        '</div>' +
        '<div style="background:rgba(0,0,0,0.7); padding:5px 15px; border-radius:30px; color:white; font-size:12px;"><span id="floor-name-' + containerId + '">' + (config.layers[0]?.name || 'Mapa') + '</span></div>' +
        '<div id="zoom-level-' + containerId + '" style="background:rgba(0,0,0,0.7); padding:5px 12px; border-radius:30px; color:#a5b4fc; font-size:12px;">100%</div>';
    
    // Viewport
    var viewport = document.createElement('div');
    viewport.style.cssText = 'width:100%; height:100%; overflow:auto; cursor:grab; background:#0f172a;';
    var camadasDiv = document.createElement('div');
    camadasDiv.style.cssText = 'position:relative; min-width:100%; min-height:100%;';
    viewport.appendChild(camadasDiv);
    
    // Navegação inferior
    var navDiv = document.createElement('div');
    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;';
    navDiv.innerHTML = '<button id="btn-prev-' + containerId + '" style="background:#334155; border:none; color:white; width:36px; height:36px; border-radius:50%; cursor:pointer;">▲</button>' +
        '<button id="btn-next-' + containerId + '" style="background:#334155; border:none; color:white; width:36px; height:36px; border-radius:50%; cursor:pointer;">▼</button>';
    
    // Coordenadas
    var coordsDiv = document.createElement('div');
    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;';
    coordsDiv.textContent = '📍 0, 0';
    
    container.appendChild(toolbar);
    container.appendChild(viewport);
    container.appendChild(coordsDiv);
    container.appendChild(navDiv);
    
    // Renderizar camadas
    function renderizarCamadas() {
        camadasDiv.innerHTML = '';
        camadas = [];
        marcadores = [];
        
        if (!config.layers || config.layers.length === 0) return;
        
        var andarExiste = false;
        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;
        
        var andarInfo = null;
        for (var i = 0; i < config.layers.length; i++) {
            if (config.layers[i].id === andarAtual) andarInfo = config.layers[i];
        }
        var nomeSpan = document.getElementById('floor-name-' + containerId);
        if (nomeSpan && andarInfo) nomeSpan.textContent = andarInfo.name;
        
        for (var i = 0; i < config.layers.length; i++) {
            var layer = config.layers[i];
            var divLayer = document.createElement('div');
            divLayer.className = 'mapa-layer';
            divLayer.setAttribute('data-floor', layer.id);
            divLayer.style.display = layer.id === andarAtual ? 'block' : 'none';
            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;
            
            var img = document.createElement('img');
            img.style.display = 'block';
            img.style.transform = 'scale(' + zoomAtual + ')';
            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);
            
            var marcadoresDiv = document.createElement('div');
            marcadoresDiv.style.position = 'absolute';
            marcadoresDiv.style.top = '0';
            marcadoresDiv.style.left = '0';
            marcadoresDiv.style.width = '100%';
            marcadoresDiv.style.height = '100%';
            marcadoresDiv.style.pointerEvents = 'none';
            
            if (layer.markers) {
                for (var j = 0; j < layer.markers.length; j++) {
                    var marker = layer.markers[j];
                    var markerDiv = criarMarcador(marker, layer.id);
                    marcadoresDiv.appendChild(markerDiv);
                    marcadores.push({ el: markerDiv, data: marker });
                }
            }
            
            divLayer.appendChild(marcadoresDiv);
            camadasDiv.appendChild(divLayer);
            camadas.push(divLayer);
        }
    }
    
    function criarMarcador(marker, floorId) {
        var div = document.createElement('div');
        div.className = 'mapa-marker';
        div.setAttribute('data-marker-id', marker.id);
        div.setAttribute('data-floor', floorId);
        div.setAttribute('data-x', marker.x);
        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';
        
        var tamanhoIcone = Math.max(20, Math.floor(20 * zoomAtual));
        var htmlIcone = marker.iconBase64 ? 
            '<img src="' + marker.iconBase64 + '" style="width:' + tamanhoIcone + 'px; height:' + tamanhoIcone + 'px;">' : 
            '<span style="font-size:' + tamanhoIcone + 'px;">📍</span>';
        
        var textoAcao = '';
        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';
        
        div.innerHTML = '<div style="display:flex; align-items:center; justify-content:center;">' + htmlIcone + '</div>' +
            '<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;">' +
            '<strong>' + (marker.name || 'Marcador') + '</strong><br><small>' + textoAcao + '</small></div>' +
            '<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>';
        
        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'; }
        });
        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'; }
        });
        
        if (marker.action && marker.action !== 'none') {
            div.addEventListener('click', function(e) {
                e.stopPropagation();
                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;
    }
    
    function atualizarPosicaoMarcadores() {
        for (var i = 0; i < marcadores.length; i++) {
            var item = marcadores[i];
            if (parseInt(item.el.getAttribute('data-floor')) === andarAtual) {
                item.el.style.left = (item.data.x * zoomAtual) + 'px';
                item.el.style.top = (item.data.y * zoomAtual) + 'px';
                var ts = Math.max(20, Math.floor(20 * zoomAtual));
                var idiv = item.el.children[0];
                if (idiv) {
                    if (item.data.iconBase64) idiv.innerHTML = '<img src="' + item.data.iconBase64 + '" style="width:' + ts + 'px; height:' + ts + 'px;">';
                    else idiv.innerHTML = '<span style="font-size:' + ts + 'px;">📍</span>';
                }
            }
        }
    }
    
    function irProximoAndar() {
        var idx = -1;
        for (var i = 0; i < config.layers.length; i++) {
            if (config.layers[i].id === andarAtual) idx = i;
        }
        if (idx < config.layers.length - 1) irParaAndar(config.layers[idx + 1].id);
    }
    
    function irAndarAnterior() {
        var idx = -1;
        for (var i = 0; i < config.layers.length; i++) {
            if (config.layers[i].id === andarAtual) idx = i;
        }
        if (idx > 0) irParaAndar(config.layers[idx - 1].id);
    }
    
    function irParaAndar(floorId) {
        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-' + containerId);
        if (nomeSpan && fd) nomeSpan.textContent = fd.name;
        atualizarPosicaoMarcadores();
    }
    
    function zoomIn() {
        if (zoomAtual < zoomMax) {
            var old = zoomAtual;
            zoomAtual = Math.min(zoomAtual + zoomPasso, zoomMax);
            aplicarZoom(old);
        }
    }
    
    function zoomOut() {
        if (zoomAtual > zoomMin) {
            var old = zoomAtual;
            zoomAtual = Math.max(zoomAtual - zoomPasso, zoomMin);
            aplicarZoom(old);
        }
    }
    
    function aplicarZoom(oldZoom) {
        var imgs = document.querySelectorAll('.mapa-image');
        for (var i = 0; i < imgs.length; i++) imgs[i].style.transform = 'scale(' + zoomAtual + ')';
        var rect = viewport.getBoundingClientRect();
        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-' + containerId);
        if (zoomSpan) zoomSpan.textContent = Math.round(zoomAtual * 100) + '%';
    }
    
    function resetarView() {
        zoomAtual = config.mapConfig.defaultZoom || 1;
        var imgs = document.querySelectorAll('.mapa-image');
        for (var i = 0; i < imgs.length; i++) imgs[i].style.transform = 'scale(' + zoomAtual + ')';
        viewport.scrollLeft = 0;
        viewport.scrollTop = 0;
        atualizarPosicaoMarcadores();
        var zoomSpan = document.getElementById('zoom-level-' + containerId);
        if (zoomSpan) zoomSpan.textContent = Math.round(zoomAtual * 100) + '%';
    }
    
    // Eventos dos botões
    document.getElementById('btn-zoom-in-' + containerId).addEventListener('click', zoomIn);
    document.getElementById('btn-zoom-out-' + containerId).addEventListener('click', zoomOut);
    document.getElementById('btn-reset-' + containerId).addEventListener('click', resetarView);
    document.getElementById('btn-prev-' + containerId).addEventListener('click', irAndarAnterior);
    document.getElementById('btn-next-' + containerId).addEventListener('click', irProximoAndar);
    
    // Arrastar mapa
    viewport.addEventListener('mousedown', function(e) {
        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);
    });
    
    window.addEventListener('mouseup', function() { arrastando = false; viewport.style.cursor = 'grab'; });
    
    // Zoom com scroll
    viewport.addEventListener('wheel', function(e) {
        if (e.ctrlKey) { e.preventDefault(); e.deltaY < 0 ? zoomIn() : zoomOut(); }
    });
    
    // Coordenadas
    viewport.addEventListener('mousemove', function(e) {
        var rect = viewport.getBoundingClientRect();
        var x = (e.clientX - rect.left + viewport.scrollLeft) / zoomAtual;
        var y = (e.clientY - rect.top + viewport.scrollTop) / zoomAtual;
        coordsDiv.textContent = '📍 ' + Math.round(x) + ', ' + Math.round(y) + ' | ' + Math.round(zoomAtual * 100) + '%';
    });
    
    renderizarCamadas();
}
]]
end

return p