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

De Wiki Gla
Ir para navegação Ir para pesquisar
(modulo lua para processar o Json do mapa)
 
Linha 1: Linha 1:
-- Módulo:MapaJson
-- Módulo:JSONHelper
-- Processa e sanitiza JSON para uso no widget MapaViewer
-- Helper para processar JSON de forma segura no MediaWiki


local p = {}
local p = {}


-- Função para escapar string para JavaScript
-- Função para escapar JSON para uso em JavaScript
function p.escapeJS(str)
function p.encodeForJS(frame)
     if not str then return '' end
     local jsonString = frame.args[1] or ''
     str = string.gsub(str, '\\', '\\\\')
      
     str = string.gsub(str, "'", "\\'")
    -- Escapar caracteres especiais para JavaScript
     str = string.gsub(str, '"', '\\"')
    jsonString = jsonString:gsub("\\", "\\\\")
     str = string.gsub(str, '\n', '\\n')
     jsonString = jsonString:gsub("'", "\\'")
     str = string.gsub(str, '\r', '\\r')
     jsonString = jsonString:gsub('"', '\\"')
     str = string.gsub(str, '\t', '\\t')
     jsonString = jsonString:gsub("\n", "\\n")
     return str
     jsonString = jsonString:gsub("\r", "\\r")
     jsonString = jsonString:gsub("\t", "\\t")
   
     return jsonString
end
end


-- Função principal para processar o JSON
-- Função para validar se o JSON é válido
function p.process(frame)
function p.validate(frame)
     local args = frame.args
     local jsonString = frame.args[1] or ''
    local jsonString = args.json or ''
      
      
     -- Se não veio JSON, tentar pegar do primeiro argumento
     -- Tentativa simples de validação
     if jsonString == '' then
     if jsonString:match("^{.*}$") or jsonString:match("^%[.*%]$") then
        jsonString = frame:getParent().args[1] or ''
        return "true"
    else
        return "false"
     end
     end
end
-- Função para minificar JSON (remover quebras de linha e espaços extras)
function p.minify(frame)
    local jsonString = frame.args[1] or ''
   
    -- Remover comentários (se houver)
    jsonString = jsonString:gsub("//.*$", "")
    jsonString = jsonString:gsub("/%*.-%*/", "")
      
      
     -- Remover quebras de linha e espaços extras
     -- Remover quebras de linha e espaços extras
     jsonString = string.gsub(jsonString, '\n', ' ')
     jsonString = jsonString:gsub("\n", "")
     jsonString = string.gsub(jsonString, '\r', ' ')
     jsonString = jsonString:gsub("\r", "")
     jsonString = string.gsub(jsonString, '\t', ' ')
     jsonString = jsonString:gsub("\t", " ")
    jsonString = jsonString:gsub("  +", " ")
      
      
     -- Remover espaços múltiplos
     -- Remover espaços entre chaves/colchetes
     jsonString = string.gsub(jsonString, '%s+', ' ')
     jsonString = jsonString:gsub("%s*({)%s*", "%1")
    jsonString = jsonString:gsub("%s*(})%s*", "%1")
    jsonString = jsonString:gsub("%s*(%[)%s*", "%1")
    jsonString = jsonString:gsub("%s*(%])%s*", "%1")
    jsonString = jsonString:gsub("%s*(:)%s*", "%1")
    jsonString = jsonString:gsub("%s*(,)%s*", "%1")
      
      
     -- Escapar para JavaScript
     return jsonString
     local escaped = p.escapeJS(jsonString)
end
 
-- Função para extrair um valor específico do JSON
function p.extractValue(frame)
    local jsonString = frame.args[1] or ''
     local key = frame.args[2] or ''
   
    -- Padrão simples para encontrar valores
    local pattern = '"' .. key .. '"%s*:%s*"([^"]*)"'
    local value = jsonString:match(pattern)
      
      
    -- Retornar como string segura
     return value or ''
     return escaped
end
end


-- Função para validar se é JSON válido
-- Função para contar camadas no JSON
function p.validate(frame)
function p.countLayers(frame)
     local jsonString = frame.args.json or ''
     local jsonString = frame.args[1] or ''
     local success, result = pcall(function()
     local count = 0
        return mw.text.jsonDecode(jsonString)
    end)
      
      
     if success then
     for _ in jsonString:gmatch('"id"%s*:%s*(%d+)') do
        return 'true'
         count = count + 1
    else
         return 'false'
     end
     end
   
    return tostring(count)
end
end


-- Função para formatar JSON bonito (opcional)
-- Função principal para gerar o código JavaScript do mapa
function p.pretty(frame)
function p.generateMapJS(frame)
     local jsonString = frame.args.json or ''
     local jsonString = frame.args[1] or ''
     local success, result = pcall(function()
    local mapId = frame.args[2] or 'mapa1'
        return mw.text.jsonDecode(jsonString)
    local width = frame.args[3] or '100%'
     end)
     local height = frame.args[4] or '500px'
    local mapName = frame.args[5] or 'Mapa'
   
    -- Minificar o JSON
    local minifiedJSON = p.minify(frame)
      
    -- Escapar para JavaScript
    local escapedJSON = p.encodeForJS({args = {minifiedJSON}})
      
      
     if success then
     -- Gerar o código JavaScript completo
        return mw.text.jsonEncode(result, mw.text.JSON_PRETTY)
    local jsCode = [=[
    else
<div id="mapa-viewer-]] .. mapId .. [=[" style="width:]] .. width .. [=[; height:]] .. height .. [=[; background:#0f172a; border-radius:12px; overflow:hidden; position:relative;">
         return 'JSON inválido'
    <div style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); color:#64748b; text-align:center;">
     end
        <div>🗺️ Carregando mapa...</div>
end
         <div style="font-size:12px; margin-top:8px;">]] .. mapName .. [=[</div>
     </div>
</div>


-- Função para extrair um campo específico
<script>
function p.getField(frame)
// @noescape
     local jsonString = frame.args.json or ''
(function() {
     local field = frame.args.field or ''
     var containerId = 'mapa-viewer-]] .. mapId .. [=[';
     var container = document.getElementById(containerId);
    if (!container) return;
      
      
     local success, data = pcall(function()
     var jsonString = ']] .. escapedJSON .. [=[';
        return mw.text.jsonDecode(jsonString)
    end)
      
      
     if success and data[field] then
     if (!jsonString || jsonString === '') {
         if type(data[field]) == 'table' then
        container.innerHTML = '<div style="padding:20px; text-align:center; color:#ef4444;">❌ Configuração não encontrada</div>';
             return mw.text.jsonEncode(data[field])
        return;
         else
    }
             return tostring(data[field])
   
         end
    var mapConfig;
     end
    try {
     return ''
        mapConfig = JSON.parse(jsonString);
    } catch(e) {
        container.innerHTML = '<div style="padding:20px; text-align:center; color:#ef4444;">❌ Erro no JSON: ' + e.message + '</div>';
        console.error('JSON inválido:', jsonString);
        return;
    }
   
    if (!mapConfig.layers || mapConfig.layers.length === 0) {
        container.innerHTML = '<div style="padding:20px; text-align:center; color:#f59e0b;">⚠️ Nenhuma camada configurada</div>';
        return;
    }
   
    iniciarMapaViewer(containerId, mapConfig);
   
    function iniciarMapaViewer(containerId, config) {
        var container = document.getElementById(containerId);
        if (!container) return;
       
        container.innerHTML = '';
        container.style.position = 'relative';
        container.style.overflow = 'hidden';
       
        // Toolbar superior
        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 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>
        `;
       
        // Viewport
        var viewport = document.createElement('div');
        viewport.className = 'mapa-viewport';
        viewport.style.cssText = 'width:100%; height:100%; overflow:auto; cursor:grab; background:#0f172a;';
       
        var layersContainer = document.createElement('div');
        layersContainer.className = 'mapa-layers';
        layersContainer.style.cssText = 'position:relative; min-width:100%; min-height:100%;';
        viewport.appendChild(layersContainer);
       
        // 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';
       
        // Navegação
        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 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>
        `;
       
        container.appendChild(toolbar);
        container.appendChild(viewport);
        container.appendChild(coordsDiv);
        container.appendChild(navDiv);
       
        // Variáveis
        var currentZoom = config.mapConfig.defaultZoom || 1;
        var currentFloor = config.mapConfig.initialFloor || 0;
        var minZoom = config.mapConfig.minZoom || 0.5;
        var maxZoom = config.mapConfig.maxZoom || 3;
        var zoomStep = config.mapConfig.zoomStep || 0.1;
        var layers = [];
        var markers = [];
        var isPanning = false;
        var panStart = { x: 0, y: 0, scrollLeft: 0, scrollTop: 0 };
       
        var floorNameSpan = toolbar.querySelector('.mapa-floor-name');
        var zoomLevelSpan = toolbar.querySelector('.mapa-zoom-level');
       
        function renderizarCamadas() {
            layersContainer.innerHTML = '';
            layers = [];
            markers = [];
           
            var floorExists = false;
            for (var i = 0; i < config.layers.length; i++) {
                if (config.layers[i].id === currentFloor) floorExists = true;
            }
            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++) {
                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 === currentFloor ? '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(' + 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(layerName) {
                    return function() {
                        var canvas = document.createElement('canvas');
                        canvas.width = 800;
                        canvas.height = 600;
                        var ctx = canvas.getContext('2d');
                        ctx.fillStyle = '#334155';
                        ctx.fillRect(0, 0, canvas.width, canvas.height);
                        ctx.fillStyle = 'white';
                        ctx.font = '20px Arial';
                        ctx.fillText(layerName || 'Andar', 50, 100);
                        this.src = canvas.toDataURL();
                    };
                })(layer.name);
               
                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);
            }
        }
       
         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 * currentZoom) + 'px';
            div.style.top = (marker.y * currentZoom) + '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 * currentZoom));
            var htmlIcone = marker.iconBase64 ?
                '<img src="' + marker.iconBase64 + '" style="width:' + tamanhoIcone + 'px; height:' + tamanhoIcone + 'px;">' :
                '<span style="font-size:' + tamanhoIcone + 'px;">📍</span>';
           
            var textoAcao = '';
            switch(marker.action) {
                case 'popup': textoAcao = '📋 Informações'; break;
                case 'nextFloor': textoAcao = '⬆️ Próximo andar'; break;
                case 'prevFloor': textoAcao = '⬇️ Andar anterior'; break;
                case 'gotoFloor': textoAcao = '🎯 Ir para andar'; break;
                case 'link': textoAcao = '🔗 Link externo'; break;
                default: 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 tooltip = this.children[1];
                if (tooltip) {
                    tooltip.style.opacity = '1';
                    tooltip.style.visibility = 'visible';
                }
            });
           
            div.addEventListener('mouseleave', function() {
                this.style.transform = 'translate(-50%, -50%) scale(1)';
                var tooltip = this.children[1];
                if (tooltip) {
                    tooltip.style.opacity = '0';
                    tooltip.style.visibility = 'hidden';
                }
            });
           
            if (marker.action && marker.action !== 'none') {
                div.addEventListener('click', function(e) {
                    e.stopPropagation();
                    executarAcao(marker);
                });
            }
           
             return div;
        }
       
        function executarAcao(marker) {
            switch(marker.action) {
                case 'popup':
                    alert(marker.name + '\n\n' + (marker.actionData?.text || 'Sem informações'));
                    break;
                case 'nextFloor':
                    proximoAndar();
                    break;
                case 'prevFloor':
                    andarAnterior();
                    break;
                case 'gotoFloor':
                    if (marker.actionData?.floorId !== undefined) {
                        irParaAndar(marker.actionData.floorId);
                    }
                    break;
                case 'link':
                    if (marker.actionData?.url && marker.actionData.url !== '#') {
                        window.open(marker.actionData.url, marker.actionData.target || '_blank');
                    }
                    break;
            }
        }
       
        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);
            }
        }
       
        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);
            }
        }
       
        function irParaAndar(floorId) {
            currentFloor = floorId;
            for (var i = 0; i < layers.length; i++) {
                var layerFloor = parseInt(layers[i].getAttribute('data-floor'));
                layers[i].style.display = layerFloor === floorId ? 'block' : 'none';
            }
            var floorData = null;
            for (var i = 0; i < config.layers.length; i++) {
                if (config.layers[i].id === floorId) floorData = config.layers[i];
            }
            if (floorData && floorNameSpan) floorNameSpan.textContent = floorData.name;
            atualizarPosicaoMarkers();
        }
       
         function atualizarPosicaoMarkers() {
            for (var i = 0; i < markers.length; i++) {
                var item = markers[i];
                if (parseInt(item.el.getAttribute('data-floor')) === currentFloor) {
                    item.el.style.left = (item.data.x * currentZoom) + 'px';
                    item.el.style.top = (item.data.y * currentZoom) + 'px';
                    var tamanhoIcone = Math.max(20, Math.floor(20 * currentZoom));
                    var iconDiv = item.el.children[0];
                    if (iconDiv) {
                        if (item.data.iconBase64) {
                            iconDiv.innerHTML = '<img src="' + item.data.iconBase64 + '" style="width:' + tamanhoIcone + 'px; height:' + tamanhoIcone + 'px;">';
                        } else {
                            iconDiv.innerHTML = '<span style="font-size:' + tamanhoIcone + 'px;">📍</span>';
                        }
                    }
                }
            }
        }
       
        function zoomIn() {
             if (currentZoom < maxZoom) {
                var oldZoom = currentZoom;
                currentZoom = Math.min(currentZoom + zoomStep, maxZoom);
                aplicarZoom(oldZoom);
            }
        }
       
        function zoomOut() {
            if (currentZoom > minZoom) {
                var oldZoom = currentZoom;
                currentZoom = Math.max(currentZoom - zoomStep, minZoom);
                aplicarZoom(oldZoom);
            }
        }
       
        function aplicarZoom(oldZoom) {
            var images = document.querySelectorAll('.mapa-image');
            for (var i = 0; i < images.length; i++) {
                images[i].style.transform = 'scale(' + currentZoom + ')';
            }
            var rect = viewport.getBoundingClientRect();
            var cx = rect.width / 2, cy = rect.height / 2;
            var sx = (viewport.scrollLeft + cx) / oldZoom;
            var sy = (viewport.scrollTop + cy) / oldZoom;
            viewport.scrollLeft = sx * currentZoom - cx;
            viewport.scrollTop = sy * currentZoom - cy;
            atualizarPosicaoMarkers();
            if (zoomLevelSpan) zoomLevelSpan.textContent = Math.round(currentZoom * 100) + '%';
        }
       
        function resetarView() {
            currentZoom = config.mapConfig.defaultZoom || 1;
            var images = document.querySelectorAll('.mapa-image');
            for (var i = 0; i < images.length; i++) {
                images[i].style.transform = 'scale(' + currentZoom + ')';
            }
            viewport.scrollLeft = 0;
            viewport.scrollTop = 0;
            atualizarPosicaoMarkers();
            if (zoomLevelSpan) zoomLevelSpan.textContent = Math.round(currentZoom * 100) + '%';
        }
       
        // Eventos dos botões
        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);
       
        // Pan
        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';
        });
       
        // Zoom com scroll
        viewport.addEventListener('wheel', function(e) {
            if (e.ctrlKey) {
                e.preventDefault();
                e.deltaY < 0 ? zoomIn() : zoomOut();
            }
        });
       
        // Tracking coordenadas
        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>]=]
      
     return jsCode
end
end


return p
return p

Edição das 09h25min de 9 de abril de 2026

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

-- Módulo:JSONHelper
-- Helper para processar JSON de forma segura no MediaWiki

local p = {}

-- Função para escapar JSON para uso em JavaScript
function p.encodeForJS(frame)
    local jsonString = frame.args[1] or ''
    
    -- Escapar caracteres especiais para JavaScript
    jsonString = jsonString:gsub("\\", "\\\\")
    jsonString = jsonString:gsub("'", "\\'")
    jsonString = jsonString:gsub('"', '\\"')
    jsonString = jsonString:gsub("\n", "\\n")
    jsonString = jsonString:gsub("\r", "\\r")
    jsonString = jsonString:gsub("\t", "\\t")
    
    return jsonString
end

-- Função para validar se o JSON é válido
function p.validate(frame)
    local jsonString = frame.args[1] or ''
    
    -- Tentativa simples de validação
    if jsonString:match("^{.*}$") or jsonString:match("^%[.*%]$") then
        return "true"
    else
        return "false"
    end
end

-- Função para minificar JSON (remover quebras de linha e espaços extras)
function p.minify(frame)
    local jsonString = frame.args[1] or ''
    
    -- Remover comentários (se houver)
    jsonString = jsonString:gsub("//.*$", "")
    jsonString = jsonString:gsub("/%*.-%*/", "")
    
    -- Remover quebras de linha e espaços extras
    jsonString = jsonString:gsub("\n", "")
    jsonString = jsonString:gsub("\r", "")
    jsonString = jsonString:gsub("\t", " ")
    jsonString = jsonString:gsub("  +", " ")
    
    -- Remover espaços entre chaves/colchetes
    jsonString = jsonString:gsub("%s*({)%s*", "%1")
    jsonString = jsonString:gsub("%s*(})%s*", "%1")
    jsonString = jsonString:gsub("%s*(%[)%s*", "%1")
    jsonString = jsonString:gsub("%s*(%])%s*", "%1")
    jsonString = jsonString:gsub("%s*(:)%s*", "%1")
    jsonString = jsonString:gsub("%s*(,)%s*", "%1")
    
    return jsonString
end

-- Função para extrair um valor específico do JSON
function p.extractValue(frame)
    local jsonString = frame.args[1] or ''
    local key = frame.args[2] or ''
    
    -- Padrão simples para encontrar valores
    local pattern = '"' .. key .. '"%s*:%s*"([^"]*)"'
    local value = jsonString:match(pattern)
    
    return value or ''
end

-- Função para contar camadas no JSON
function p.countLayers(frame)
    local jsonString = frame.args[1] or ''
    local count = 0
    
    for _ in jsonString:gmatch('"id"%s*:%s*(%d+)') do
        count = count + 1
    end
    
    return tostring(count)
end

-- Função principal para gerar o código JavaScript do mapa
function p.generateMapJS(frame)
    local jsonString = frame.args[1] or ''
    local mapId = frame.args[2] or 'mapa1'
    local width = frame.args[3] or '100%'
    local height = frame.args[4] or '500px'
    local mapName = frame.args[5] or 'Mapa'
    
    -- Minificar o JSON
    local minifiedJSON = p.minify(frame)
    
    -- Escapar para JavaScript
    local escapedJSON = p.encodeForJS({args = {minifiedJSON}})
    
    -- Gerar o código JavaScript completo
    local jsCode = [=[
<div id="mapa-viewer-]] .. mapId .. [=[" style="width:]] .. width .. [=[; height:]] .. height .. [=[; 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>🗺️ Carregando mapa...</div>
        <div style="font-size:12px; margin-top:8px;">]] .. mapName .. [=[</div>
    </div>
</div>

<script>
// @noescape
(function() {
    var containerId = 'mapa-viewer-]] .. mapId .. [=[';
    var container = document.getElementById(containerId);
    if (!container) return;
    
    var jsonString = ']] .. escapedJSON .. [=[';
    
    if (!jsonString || jsonString === '') {
        container.innerHTML = '<div style="padding:20px; text-align:center; color:#ef4444;">❌ Configuração não encontrada</div>';
        return;
    }
    
    var mapConfig;
    try {
        mapConfig = JSON.parse(jsonString);
    } catch(e) {
        container.innerHTML = '<div style="padding:20px; text-align:center; color:#ef4444;">❌ Erro no JSON: ' + e.message + '</div>';
        console.error('JSON inválido:', jsonString);
        return;
    }
    
    if (!mapConfig.layers || mapConfig.layers.length === 0) {
        container.innerHTML = '<div style="padding:20px; text-align:center; color:#f59e0b;">⚠️ Nenhuma camada configurada</div>';
        return;
    }
    
    iniciarMapaViewer(containerId, mapConfig);
    
    function iniciarMapaViewer(containerId, config) {
        var container = document.getElementById(containerId);
        if (!container) return;
        
        container.innerHTML = '';
        container.style.position = 'relative';
        container.style.overflow = 'hidden';
        
        // Toolbar superior
        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 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>
        `;
        
        // Viewport
        var viewport = document.createElement('div');
        viewport.className = 'mapa-viewport';
        viewport.style.cssText = 'width:100%; height:100%; overflow:auto; cursor:grab; background:#0f172a;';
        
        var layersContainer = document.createElement('div');
        layersContainer.className = 'mapa-layers';
        layersContainer.style.cssText = 'position:relative; min-width:100%; min-height:100%;';
        viewport.appendChild(layersContainer);
        
        // 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';
        
        // Navegação
        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 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>
        `;
        
        container.appendChild(toolbar);
        container.appendChild(viewport);
        container.appendChild(coordsDiv);
        container.appendChild(navDiv);
        
        // Variáveis
        var currentZoom = config.mapConfig.defaultZoom || 1;
        var currentFloor = config.mapConfig.initialFloor || 0;
        var minZoom = config.mapConfig.minZoom || 0.5;
        var maxZoom = config.mapConfig.maxZoom || 3;
        var zoomStep = config.mapConfig.zoomStep || 0.1;
        var layers = [];
        var markers = [];
        var isPanning = false;
        var panStart = { x: 0, y: 0, scrollLeft: 0, scrollTop: 0 };
        
        var floorNameSpan = toolbar.querySelector('.mapa-floor-name');
        var zoomLevelSpan = toolbar.querySelector('.mapa-zoom-level');
        
        function renderizarCamadas() {
            layersContainer.innerHTML = '';
            layers = [];
            markers = [];
            
            var floorExists = false;
            for (var i = 0; i < config.layers.length; i++) {
                if (config.layers[i].id === currentFloor) floorExists = true;
            }
            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++) {
                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 === currentFloor ? '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(' + 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(layerName) {
                    return function() {
                        var canvas = document.createElement('canvas');
                        canvas.width = 800;
                        canvas.height = 600;
                        var ctx = canvas.getContext('2d');
                        ctx.fillStyle = '#334155';
                        ctx.fillRect(0, 0, canvas.width, canvas.height);
                        ctx.fillStyle = 'white';
                        ctx.font = '20px Arial';
                        ctx.fillText(layerName || 'Andar', 50, 100);
                        this.src = canvas.toDataURL();
                    };
                })(layer.name);
                
                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);
            }
        }
        
        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 * currentZoom) + 'px';
            div.style.top = (marker.y * currentZoom) + '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 * currentZoom));
            var htmlIcone = marker.iconBase64 ? 
                '<img src="' + marker.iconBase64 + '" style="width:' + tamanhoIcone + 'px; height:' + tamanhoIcone + 'px;">' : 
                '<span style="font-size:' + tamanhoIcone + 'px;">📍</span>';
            
            var textoAcao = '';
            switch(marker.action) {
                case 'popup': textoAcao = '📋 Informações'; break;
                case 'nextFloor': textoAcao = '⬆️ Próximo andar'; break;
                case 'prevFloor': textoAcao = '⬇️ Andar anterior'; break;
                case 'gotoFloor': textoAcao = '🎯 Ir para andar'; break;
                case 'link': textoAcao = '🔗 Link externo'; break;
                default: 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 tooltip = this.children[1];
                if (tooltip) {
                    tooltip.style.opacity = '1';
                    tooltip.style.visibility = 'visible';
                }
            });
            
            div.addEventListener('mouseleave', function() {
                this.style.transform = 'translate(-50%, -50%) scale(1)';
                var tooltip = this.children[1];
                if (tooltip) {
                    tooltip.style.opacity = '0';
                    tooltip.style.visibility = 'hidden';
                }
            });
            
            if (marker.action && marker.action !== 'none') {
                div.addEventListener('click', function(e) {
                    e.stopPropagation();
                    executarAcao(marker);
                });
            }
            
            return div;
        }
        
        function executarAcao(marker) {
            switch(marker.action) {
                case 'popup':
                    alert(marker.name + '\n\n' + (marker.actionData?.text || 'Sem informações'));
                    break;
                case 'nextFloor':
                    proximoAndar();
                    break;
                case 'prevFloor':
                    andarAnterior();
                    break;
                case 'gotoFloor':
                    if (marker.actionData?.floorId !== undefined) {
                        irParaAndar(marker.actionData.floorId);
                    }
                    break;
                case 'link':
                    if (marker.actionData?.url && marker.actionData.url !== '#') {
                        window.open(marker.actionData.url, marker.actionData.target || '_blank');
                    }
                    break;
            }
        }
        
        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);
            }
        }
        
        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);
            }
        }
        
        function irParaAndar(floorId) {
            currentFloor = floorId;
            for (var i = 0; i < layers.length; i++) {
                var layerFloor = parseInt(layers[i].getAttribute('data-floor'));
                layers[i].style.display = layerFloor === floorId ? 'block' : 'none';
            }
            var floorData = null;
            for (var i = 0; i < config.layers.length; i++) {
                if (config.layers[i].id === floorId) floorData = config.layers[i];
            }
            if (floorData && floorNameSpan) floorNameSpan.textContent = floorData.name;
            atualizarPosicaoMarkers();
        }
        
        function atualizarPosicaoMarkers() {
            for (var i = 0; i < markers.length; i++) {
                var item = markers[i];
                if (parseInt(item.el.getAttribute('data-floor')) === currentFloor) {
                    item.el.style.left = (item.data.x * currentZoom) + 'px';
                    item.el.style.top = (item.data.y * currentZoom) + 'px';
                    var tamanhoIcone = Math.max(20, Math.floor(20 * currentZoom));
                    var iconDiv = item.el.children[0];
                    if (iconDiv) {
                        if (item.data.iconBase64) {
                            iconDiv.innerHTML = '<img src="' + item.data.iconBase64 + '" style="width:' + tamanhoIcone + 'px; height:' + tamanhoIcone + 'px;">';
                        } else {
                            iconDiv.innerHTML = '<span style="font-size:' + tamanhoIcone + 'px;">📍</span>';
                        }
                    }
                }
            }
        }
        
        function zoomIn() {
            if (currentZoom < maxZoom) {
                var oldZoom = currentZoom;
                currentZoom = Math.min(currentZoom + zoomStep, maxZoom);
                aplicarZoom(oldZoom);
            }
        }
        
        function zoomOut() {
            if (currentZoom > minZoom) {
                var oldZoom = currentZoom;
                currentZoom = Math.max(currentZoom - zoomStep, minZoom);
                aplicarZoom(oldZoom);
            }
        }
        
        function aplicarZoom(oldZoom) {
            var images = document.querySelectorAll('.mapa-image');
            for (var i = 0; i < images.length; i++) {
                images[i].style.transform = 'scale(' + currentZoom + ')';
            }
            var rect = viewport.getBoundingClientRect();
            var cx = rect.width / 2, cy = rect.height / 2;
            var sx = (viewport.scrollLeft + cx) / oldZoom;
            var sy = (viewport.scrollTop + cy) / oldZoom;
            viewport.scrollLeft = sx * currentZoom - cx;
            viewport.scrollTop = sy * currentZoom - cy;
            atualizarPosicaoMarkers();
            if (zoomLevelSpan) zoomLevelSpan.textContent = Math.round(currentZoom * 100) + '%';
        }
        
        function resetarView() {
            currentZoom = config.mapConfig.defaultZoom || 1;
            var images = document.querySelectorAll('.mapa-image');
            for (var i = 0; i < images.length; i++) {
                images[i].style.transform = 'scale(' + currentZoom + ')';
            }
            viewport.scrollLeft = 0;
            viewport.scrollTop = 0;
            atualizarPosicaoMarkers();
            if (zoomLevelSpan) zoomLevelSpan.textContent = Math.round(currentZoom * 100) + '%';
        }
        
        // Eventos dos botões
        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);
        
        // Pan
        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';
        });
        
        // Zoom com scroll
        viewport.addEventListener('wheel', function(e) {
            if (e.ctrlKey) {
                e.preventDefault();
                e.deltaY < 0 ? zoomIn() : zoomOut();
            }
        });
        
        // Tracking coordenadas
        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>]=]
    
    return jsCode
end

return p