Mudanças entre as edições de "Módulo:MapaJson"
Ir para navegação
Ir para pesquisar
| Linha 16: | Linha 16: | ||
end | end | ||
-- Função para minificar JSON | -- Função para minificar JSON | ||
function p.minifyJSON(json) | function p.minifyJSON(json) | ||
if not json then return '' end | if not json then return '' end | ||
json = json:gsub("\n", "") | json = json:gsub("\n", "") | ||
json = json:gsub("\r", "") | json = json:gsub("\r", "") | ||
json = json:gsub("\t", " ") | json = json:gsub("\t", " ") | ||
json = json:gsub(" +", " ") | 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") | ||
| Linha 37: | Linha 29: | ||
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 | return json | ||
end | end | ||
-- Função principal | -- Função principal | ||
function p.render(frame) | function p.render(frame) | ||
local args = frame.args | local args = frame.args | ||
local jsonString = args.json or '' | local jsonString = args.json or '' | ||
| Linha 51: | Linha 41: | ||
local mapName = args.nome or 'Mapa' | local mapName = args.nome or 'Mapa' | ||
-- Minificar e escapar | |||
-- Minificar | |||
local minifiedJSON = p.minifyJSON(jsonString) | local minifiedJSON = p.minifyJSON(jsonString) | ||
local escapedJSON = p.escapeJS(minifiedJSON) | local escapedJSON = p.escapeJS(minifiedJSON) | ||
-- Gerar | -- Gerar HTML/JS | ||
local | local output = [[ | ||
<div id="mapa-viewer-]] .. mapId .. [[" style="width:]] .. width .. [[; height:]] .. height .. [[; background:#0f172a; border-radius:12px; overflow:hidden; position:relative;"> | <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 style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); color:#64748b; text-align:center;"> | ||
| Linha 76: | Linha 53: | ||
</div> | </div> | ||
</div> | </div> | ||
<script> | <script> | ||
(function() { | (function() { | ||
| Linha 82: | Linha 58: | ||
var container = document.getElementById(containerId); | var container = document.getElementById(containerId); | ||
if (!container) return; | if (!container) return; | ||
var jsonString = ']] .. escapedJSON .. [['; | var jsonString = ']] .. escapedJSON .. [['; | ||
if (!jsonString || jsonString === '') { | if (!jsonString || jsonString === '') { | ||
container.innerHTML = '<div style="padding:20px; text-align:center; color:#ef4444;">❌ Configuração não encontrada</div>'; | container.innerHTML = '<div style="padding:20px; text-align:center; color:#ef4444;">❌ Configuração não encontrada</div>'; | ||
return; | return; | ||
} | } | ||
var mapConfig; | var mapConfig; | ||
try { | try { | ||
| Linha 97: | Linha 70: | ||
return; | return; | ||
} | } | ||
if (!mapConfig.layers || mapConfig.layers.length === 0) { | if (!mapConfig.layers || mapConfig.layers.length === 0) { | ||
container.innerHTML = '<div style="padding:20px; text-align:center; color:#f59e0b;">⚠️ Nenhuma camada configurada</div>'; | container.innerHTML = '<div style="padding:20px; text-align:center; color:#f59e0b;">⚠️ Nenhuma camada configurada</div>'; | ||
return; | return; | ||
} | } | ||
iniciarMapaViewer(containerId, mapConfig); | iniciarMapaViewer(containerId, mapConfig); | ||
| Linha 108: | Linha 79: | ||
var container = document.getElementById(containerId); | var container = document.getElementById(containerId); | ||
if (!container) return; | if (!container) return; | ||
container.innerHTML = ''; | container.innerHTML = ''; | ||
container.style.position = 'relative'; | container.style.position = 'relative'; | ||
container.style.overflow = 'hidden'; | container.style.overflow = 'hidden'; | ||
var toolbar = document.createElement('div'); | 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.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 = | 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 viewport = document.createElement('div'); | ||
viewport.className = 'mapa-viewport'; | viewport.className = 'mapa-viewport'; | ||
viewport.style.cssText = 'width:100%; height:100%; overflow:auto; cursor:grab; background:#0f172a;'; | viewport.style.cssText = 'width:100%; height:100%; overflow:auto; cursor:grab; background:#0f172a;'; | ||
var layersContainer = document.createElement('div'); | var layersContainer = document.createElement('div'); | ||
layersContainer.className = 'mapa-layers'; | layersContainer.className = 'mapa-layers'; | ||
| Linha 138: | Linha 95: | ||
viewport.appendChild(layersContainer); | viewport.appendChild(layersContainer); | ||
var coordsDiv = document.createElement('div'); | 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.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'; | coordsDiv.textContent = '📍 0, 0'; | ||
var navDiv = document.createElement('div'); | 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.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 = | 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(toolbar); | ||
| Linha 156: | Linha 108: | ||
container.appendChild(navDiv); | container.appendChild(navDiv); | ||
var currentZoom = config.mapConfig.defaultZoom || 1; | var currentZoom = config.mapConfig.defaultZoom || 1; | ||
var currentFloor = config.mapConfig.initialFloor || 0; | var currentFloor = config.mapConfig.initialFloor || 0; | ||
| Linha 166: | Linha 117: | ||
var isPanning = false; | var isPanning = false; | ||
var panStart = { x: 0, y: 0, scrollLeft: 0, scrollTop: 0 }; | var panStart = { x: 0, y: 0, scrollLeft: 0, scrollTop: 0 }; | ||
var floorNameSpan = toolbar.querySelector('.mapa-floor-name'); | var floorNameSpan = toolbar.querySelector('.mapa-floor-name'); | ||
var zoomLevelSpan = toolbar.querySelector('.mapa-zoom-level'); | var zoomLevelSpan = toolbar.querySelector('.mapa-zoom-level'); | ||
| Linha 174: | Linha 124: | ||
layers = []; | layers = []; | ||
markers = []; | markers = []; | ||
var floorExists = false; | var floorExists = false; | ||
for (var i = 0; i < config.layers.length; i++) { | for (var i = 0; i < config.layers.length; i++) { | ||
if (config.layers[i].id === currentFloor) floorExists = true; | if (config.layers[i].id === currentFloor) floorExists = true; | ||
} | } | ||
if (!floorExists && config.layers.length > 0) | if (!floorExists && config.layers.length > 0) currentFloor = config.layers[0].id; | ||
var floorAtual = null; | var floorAtual = null; | ||
for (var i = 0; i < config.layers.length; i++) { | for (var i = 0; i < config.layers.length; i++) { | ||
if (config.layers[i].id === currentFloor) floorAtual = config.layers[i]; | if (config.layers[i].id === currentFloor) floorAtual = config.layers[i]; | ||
} | } | ||
if (floorAtual && floorNameSpan) | if (floorAtual && floorNameSpan) floorNameSpan.textContent = floorAtual.name; | ||
for (var i = 0; i < config.layers.length; i++) { | for (var i = 0; i < config.layers.length; i++) { | ||
| Linha 208: | Linha 152: | ||
img.style.transformOrigin = '0 0'; | img.style.transformOrigin = '0 0'; | ||
img.src = layer.imagePath || ''; | img.src = layer.imagePath || ''; | ||
img.onload = function() { layersContainer.style.width = this.width + 'px'; layersContainer.style.height = this.height + 'px'; }; | |||
img.onload = function() { | 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'; }; | ||
img.onerror = | |||
divLayer.appendChild(img); | divLayer.appendChild(img); | ||
| Linha 247: | Linha 172: | ||
} | } | ||
} | } | ||
divLayer.appendChild(markersDiv); | divLayer.appendChild(markersDiv); | ||
layersContainer.appendChild(divLayer); | layersContainer.appendChild(divLayer); | ||
| Linha 261: | Linha 185: | ||
div.setAttribute('data-x', marker.x); | div.setAttribute('data-x', marker.x); | ||
div.setAttribute('data-y', marker.y); | div.setAttribute('data-y', marker.y); | ||
div.style.position = 'absolute'; | div.style.position = 'absolute'; | ||
div.style.left = (marker.x * currentZoom) + 'px'; | div.style.left = (marker.x * currentZoom) + 'px'; | ||
| Linha 268: | Linha 191: | ||
div.style.transform = 'translate(-50%, -50%)'; | div.style.transform = 'translate(-50%, -50%)'; | ||
div.style.cursor = (marker.action && marker.action !== 'none') ? 'pointer' : 'default'; | div.style.cursor = (marker.action && marker.action !== 'none') ? 'pointer' : 'default'; | ||
var tamanhoIcone = Math.max(20, Math.floor(20 * currentZoom)); | var tamanhoIcone = Math.max(20, Math.floor(20 * currentZoom)); | ||
var htmlIcone = marker.iconBase64 ? | var htmlIcone = marker.iconBase64 ? '<img src="' + marker.iconBase64 + '" style="width:' + tamanhoIcone + 'px; height:' + tamanhoIcone + 'px;">' : '<span style="font-size:' + tamanhoIcone + 'px;">📍</span>'; | ||
var textoAcao = ''; | 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.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') { | if (marker.action && marker.action !== 'none') { | ||
div.addEventListener('click', function(e) { | div.addEventListener('click', function(e) { e.stopPropagation(); executarAcao(marker); }); | ||
} | } | ||
return div; | return div; | ||
} | } | ||
function executarAcao(marker) { | function executarAcao(marker) { | ||
if (marker.action === 'popup') alert(marker.name + '\n\n' + (marker.actionData?.text || 'Sem informações')); | |||
else if (marker.action === 'nextFloor') proximoAndar(); | |||
else if (marker.action === 'prevFloor') andarAnterior(); | |||
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() { | 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 andarAnterior() { | |||
function irParaAndar(floorId) { | function irParaAndar(floorId) { | ||
currentFloor = floorId; | currentFloor = floorId; | ||
for (var i = 0; i < layers.length; i++) { | for (var i = 0; i < layers.length; i++) { var lf = parseInt(layers[i].getAttribute('data-floor')); layers[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]; } | |||
if (fd && floorNameSpan) floorNameSpan.textContent = fd.name; | |||
var | |||
if ( | |||
atualizarPosicaoMarkers(); | atualizarPosicaoMarkers(); | ||
} | } | ||
| Linha 385: | Linha 236: | ||
item.el.style.left = (item.data.x * currentZoom) + 'px'; | item.el.style.left = (item.data.x * currentZoom) + 'px'; | ||
item.el.style.top = (item.data.y * currentZoom) + 'px'; | item.el.style.top = (item.data.y * currentZoom) + 'px'; | ||
var | var ts = Math.max(20, Math.floor(20 * currentZoom)); | ||
var | var idiv = item.el.children[0]; | ||
if ( | if (idiv) { | ||
if (item.data.iconBase64) | 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>'; | |||
} | } | ||
} | } | ||
| Linha 398: | Linha 246: | ||
} | } | ||
function zoomIn() { | 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 zoomOut() { | |||
function aplicarZoom(oldZoom) { | function aplicarZoom(oldZoom) { | ||
var | var imgs = document.querySelectorAll('.mapa-image'); | ||
for (var i = 0; i < | for (var i = 0; i < imgs.length; i++) imgs[i].style.transform = 'scale(' + currentZoom + ')'; | ||
var rect = viewport.getBoundingClientRect(); | var rect = viewport.getBoundingClientRect(); | ||
var cx = rect.width / 2, cy = rect.height / 2; | var cx = rect.width / 2, cy = rect.height / 2; | ||
var sx = (viewport.scrollLeft + cx) / oldZoom | var sx = (viewport.scrollLeft + cx) / oldZoom, sy = (viewport.scrollTop + cy) / oldZoom; | ||
viewport.scrollLeft = sx * currentZoom - cx; | viewport.scrollLeft = sx * currentZoom - cx; | ||
viewport.scrollTop = sy * currentZoom - cy; | viewport.scrollTop = sy * currentZoom - cy; | ||
| Linha 431: | Linha 263: | ||
function resetarView() { | function resetarView() { | ||
currentZoom = config.mapConfig.defaultZoom || 1; | currentZoom = config.mapConfig.defaultZoom || 1; | ||
var | var imgs = document.querySelectorAll('.mapa-image'); | ||
for (var i = 0; i < | for (var i = 0; i < imgs.length; i++) imgs[i].style.transform = 'scale(' + currentZoom + ')'; | ||
viewport.scrollLeft = 0; | viewport.scrollLeft = 0; | ||
viewport.scrollTop = 0; | viewport.scrollTop = 0; | ||
| Linha 441: | Linha 271: | ||
} | } | ||
var btnZoomIn = toolbar.querySelector('.mapa-zoom-in'); | var btnZoomIn = toolbar.querySelector('.mapa-zoom-in'); | ||
var btnZoomOut = toolbar.querySelector('.mapa-zoom-out'); | var btnZoomOut = toolbar.querySelector('.mapa-zoom-out'); | ||
| Linha 447: | Linha 276: | ||
var btnPrev = navDiv.querySelector('.mapa-prev-floor'); | var btnPrev = navDiv.querySelector('.mapa-prev-floor'); | ||
var btnNext = navDiv.querySelector('.mapa-next-floor'); | var btnNext = navDiv.querySelector('.mapa-next-floor'); | ||
if (btnZoomIn) btnZoomIn.addEventListener('click', zoomIn); | if (btnZoomIn) btnZoomIn.addEventListener('click', zoomIn); | ||
if (btnZoomOut) btnZoomOut.addEventListener('click', zoomOut); | if (btnZoomOut) btnZoomOut.addEventListener('click', zoomOut); | ||
| Linha 454: | Linha 282: | ||
if (btnNext) btnNext.addEventListener('click', proximoAndar); | 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(); }); | |||
viewport.addEventListener('mousedown', function(e) { | 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) + '%'; }); | |||
window.addEventListener('mousemove', function(e) { | |||
window.addEventListener('mouseup', function() { | |||
viewport.addEventListener('wheel', function(e) { | |||
viewport.addEventListener('mousemove', function(e) { | |||
renderizarCamadas(); | renderizarCamadas(); | ||
| Linha 495: | Linha 293: | ||
</script>]] | </script>]] | ||
return html | -- Usar frame:extensionTag para garantir que o HTML seja processado corretamente | ||
return frame:extensionTag('html', output) | |||
end | end | ||
return p | return p | ||
Edição das 10h09min de 9 de abril de 2026
A documentação para este módulo pode ser criada em Módulo:MapaJson/doc
-- Módulo:MapaJson
-- Processa JSON para o visualizador de mapas
local p = {}
-- Função para escapar string para JavaScript
function p.escapeJS(str)
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 jsonString = args.json or ''
local mapId = args.id or 'mapa1'
local width = args.largura or '100%'
local height = args.altura or '500px'
local mapName = args.nome or 'Mapa'
-- Minificar e escapar
local minifiedJSON = p.minifyJSON(jsonString)
local escapedJSON = p.escapeJS(minifiedJSON)
-- Gerar HTML/JS
local output = [[
<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>
(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>';
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';
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>';
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);
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';
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);
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() { 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);
}
}
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 = '';
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(); executarAcao(marker); });
}
return div;
}
function executarAcao(marker) {
if (marker.action === 'popup') alert(marker.name + '\n\n' + (marker.actionData?.text || 'Sem informações'));
else if (marker.action === 'nextFloor') proximoAndar();
else if (marker.action === 'prevFloor') andarAnterior();
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); }
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 lf = parseInt(layers[i].getAttribute('data-floor')); layers[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]; }
if (fd && floorNameSpan) floorNameSpan.textContent = fd.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 ts = Math.max(20, Math.floor(20 * currentZoom));
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 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 aplicarZoom(oldZoom) {
var imgs = document.querySelectorAll('.mapa-image');
for (var i = 0; i < imgs.length; i++) imgs[i].style.transform = 'scale(' + currentZoom + ')';
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 * currentZoom - cx;
viewport.scrollTop = sy * currentZoom - cy;
atualizarPosicaoMarkers();
if (zoomLevelSpan) zoomLevelSpan.textContent = Math.round(currentZoom * 100) + '%';
}
function resetarView() {
currentZoom = config.mapConfig.defaultZoom || 1;
var imgs = document.querySelectorAll('.mapa-image');
for (var i = 0; i < imgs.length; i++) imgs[i].style.transform = 'scale(' + currentZoom + ')';
viewport.scrollLeft = 0;
viewport.scrollTop = 0;
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
return frame:extensionTag('html', output)
end
return p