Mudanças entre as edições de "Módulo:MapaJson"
Ir para navegação
Ir para pesquisar
(Página substituída por 'local p = {} function p.ola(frame) return "Lua está funcionando!" end return p') Etiqueta: Substituído |
|||
| Linha 1: | Linha 1: | ||
-- Módulo:MapaJson | |||
-- Processa JSON para o visualizador de mapas | |||
local p = {} | local p = {} | ||
function p. | -- Função para escapar string para JavaScript | ||
return " | 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 (remover espaços e quebras) | |||
function p.minifyJSON(json) | |||
if not json then return '' end | |||
-- Remover comentários | |||
json = json:gsub("//[^\n]*", "") | |||
json = json:gsub("/%*.-%*/", "") | |||
-- Remover quebras de linha e espaços extras | |||
json = json:gsub("\n", "") | |||
json = json:gsub("\r", "") | |||
json = json:gsub("\t", " ") | |||
json = json:gsub(" +", " ") | |||
-- Remover espaços entre caracteres especiais | |||
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 que gera o widget completo | |||
function p.render(frame) | |||
-- Pegar os parâmetros | |||
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' | |||
-- Se o JSON veio de uma página ({{:Página}}), buscar o conteúdo | |||
if jsonString:match("^%{%{:[^}]+%}%}$") then | |||
local pageName = jsonString:match("^%{%{:([^}]+)%}%}$") | |||
if pageName then | |||
local title = mw.title.new(pageName) | |||
if title then | |||
jsonString = title:getContent() or '' | |||
end | |||
end | |||
end | |||
-- Minificar o JSON | |||
local minifiedJSON = p.minifyJSON(jsonString) | |||
-- Escapar para JavaScript | |||
local escapedJSON = p.escapeJS(minifiedJSON) | |||
-- Gerar o HTML/JS completo | |||
local html = [[ | |||
<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'; | |||
// 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 html | |||
end | end | ||
return p | return p | ||
Edição das 09h57min 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 (remover espaços e quebras)
function p.minifyJSON(json)
if not json then return '' end
-- Remover comentários
json = json:gsub("//[^\n]*", "")
json = json:gsub("/%*.-%*/", "")
-- Remover quebras de linha e espaços extras
json = json:gsub("\n", "")
json = json:gsub("\r", "")
json = json:gsub("\t", " ")
json = json:gsub(" +", " ")
-- Remover espaços entre caracteres especiais
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 que gera o widget completo
function p.render(frame)
-- Pegar os parâmetros
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'
-- Se o JSON veio de uma página ({{:Página}}), buscar o conteúdo
if jsonString:match("^%{%{:[^}]+%}%}$") then
local pageName = jsonString:match("^%{%{:([^}]+)%}%}$")
if pageName then
local title = mw.title.new(pageName)
if title then
jsonString = title:getContent() or ''
end
end
end
-- Minificar o JSON
local minifiedJSON = p.minifyJSON(jsonString)
-- Escapar para JavaScript
local escapedJSON = p.escapeJS(minifiedJSON)
-- Gerar o HTML/JS completo
local html = [[
<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';
// 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 html
end
return p