Mudanças entre as edições de "Módulo:MapaJson"
Ir para navegação
Ir para pesquisar
| Linha 1: | Linha 1: | ||
local p = {} | local p = {} | ||
function p.renderizar(frame) | |||
function p. | -- Pegar os parâmetros | ||
-- | |||
local args = frame.args | local args = frame.args | ||
local | local mapaId = args.id or 'mapa1' | ||
local | local largura = args.largura or '100%' | ||
local | local altura = args.altura or '500px' | ||
local | local titulo = args.titulo or 'Mapa' | ||
local | local jsonFonte = args.json or '' | ||
-- | -- Se o JSON veio de uma página (ex: {{:Mapa:Config}}), buscar o conteúdo | ||
local | if jsonFonte:match("^%{%{:[^}]+%}%}$") then | ||
local nomePagina = jsonFonte:match("^%{%{:([^}]+)%}%}$") | |||
if nomePagina then | |||
local title = mw.title.new(nomePagina) | |||
if title and title.exists then | |||
jsonFonte = title:getContent() or '' | |||
end | |||
end | |||
end | |||
-- Gerar HTML | -- Limpar o JSON (remover quebras de linha e espaços extras) | ||
local | jsonFonte = jsonFonte:gsub("\n", "") | ||
<div id="mapa- | jsonFonte = jsonFonte:gsub("\r", "") | ||
jsonFonte = jsonFonte:gsub("\t", " ") | |||
jsonFonte = jsonFonte:gsub(" +", " ") | |||
-- Escapar para usar dentro de string JavaScript | |||
local jsonEscapado = jsonFonte:gsub("'", "\\'") | |||
-- Gerar o HTML com o visualizador | |||
local html = [[ | |||
<div id="mapa-container-]] .. mapaId .. [[" style="width:]] .. largura .. [[; height:]] .. altura .. [[; background:#0f172a; border-radius:12px; overflow:hidden; position:relative;"> | |||
<div style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); color:#64748b; text-align:center;"> | <div style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); color:#64748b; text-align:center;"> | ||
🗺️ Carregando mapa...<br> | |||
< | <small>]] .. titulo .. [[</small> | ||
</div> | </div> | ||
</div> | </div> | ||
<script> | <script> | ||
(function() { | (function() { | ||
var containerId = 'mapa- | var containerId = 'mapa-container-]] .. mapaId .. [['; | ||
var container = document.getElementById(containerId); | var container = document.getElementById(containerId); | ||
if (!container) return; | if (!container) return; | ||
var jsonString = ']] .. | |||
var jsonString = ']] .. jsonEscapado .. [['; | |||
]] .. p.obterCodigoJS() .. [[ | |||
try { | try { | ||
var config = JSON.parse(jsonString); | |||
if (config.layers && config.layers.length > 0) { | |||
carregarMapa(containerId, config); | |||
} else { | |||
container.innerHTML = '<div style="padding:20px; text-align:center; color:#f59e0b;">⚠️ Nenhuma camada encontrada</div>'; | |||
} | |||
} catch(e) { | } catch(e) { | ||
container.innerHTML = '<div style="padding:20px; text-align:center; color:#ef4444;">❌ Erro | container.innerHTML = '<div style="padding:20px; text-align:center; color:#ef4444;">❌ Erro ao carregar mapa: ' + e.message + '</div>'; | ||
} | } | ||
if (!mapConfig. | })(); | ||
</script>]] | |||
return html | |||
end | |||
function p.obterCodigoJS() | |||
return [[ | |||
function carregarMapa(containerId, config) { | |||
var container = document.getElementById(containerId); | |||
if (!container) return; | |||
container.innerHTML = ''; | |||
container.style.position = 'relative'; | |||
container.style.overflow = 'hidden'; | |||
// Zoom e navegação | |||
var zoomAtual = config.mapConfig.defaultZoom || 1; | |||
var andarAtual = config.mapConfig.initialFloor || 0; | |||
var zoomMin = config.mapConfig.minZoom || 0.5; | |||
var zoomMax = config.mapConfig.maxZoom || 3; | |||
var zoomPasso = config.mapConfig.zoomStep || 0.1; | |||
var camadas = []; | |||
var marcadores = []; | |||
var arrastando = false; | |||
var arrasteInicio = { x: 0, y: 0, scrollLeft: 0, scrollTop: 0 }; | |||
// Criar toolbar | |||
var toolbar = document.createElement('div'); | |||
toolbar.style.cssText = 'position:absolute; top:10px; left:10px; right:10px; z-index:100; display:flex; justify-content:space-between; gap:8px; flex-wrap:wrap;'; | |||
toolbar.innerHTML = '<div style="display:flex; gap:5px; background:rgba(0,0,0,0.7); padding:5px 10px; border-radius:30px;">' + | |||
'<button id="btn-zoom-in-' + containerId + '" style="background:#334155; border:none; color:white; width:32px; height:32px; border-radius:50%; cursor:pointer;">+</button>' + | |||
'<button id="btn-zoom-out-' + containerId + '" style="background:#334155; border:none; color:white; width:32px; height:32px; border-radius:50%; cursor:pointer;">-</button>' + | |||
'<button id="btn-reset-' + containerId + '" style="background:#334155; border:none; color:white; width:32px; height:32px; border-radius:50%; cursor:pointer;">⟳</button>' + | |||
'</div>' + | |||
'<div style="background:rgba(0,0,0,0.7); padding:5px 15px; border-radius:30px; color:white; font-size:12px;"><span id="floor-name-' + containerId + '">' + (config.layers[0]?.name || 'Mapa') + '</span></div>' + | |||
'<div id="zoom-level-' + containerId + '" style="background:rgba(0,0,0,0.7); padding:5px 12px; border-radius:30px; color:#a5b4fc; font-size:12px;">100%</div>'; | |||
// Viewport | |||
var viewport = document.createElement('div'); | |||
viewport.style.cssText = 'width:100%; height:100%; overflow:auto; cursor:grab; background:#0f172a;'; | |||
var camadasDiv = document.createElement('div'); | |||
camadasDiv.style.cssText = 'position:relative; min-width:100%; min-height:100%;'; | |||
viewport.appendChild(camadasDiv); | |||
// Navegação inferior | |||
var navDiv = document.createElement('div'); | |||
navDiv.style.cssText = 'position:absolute; bottom:20px; left:20px; z-index:100; display:flex; gap:8px; background:rgba(0,0,0,0.7); padding:8px; border-radius:40px;'; | |||
navDiv.innerHTML = '<button id="btn-prev-' + containerId + '" style="background:#334155; border:none; color:white; width:36px; height:36px; border-radius:50%; cursor:pointer;">▲</button>' + | |||
'<button id="btn-next-' + containerId + '" style="background:#334155; border:none; color:white; width:36px; height:36px; border-radius:50%; cursor:pointer;">▼</button>'; | |||
// Coordenadas | |||
var coordsDiv = document.createElement('div'); | |||
coordsDiv.style.cssText = 'position:absolute; bottom:10px; right:10px; background:rgba(0,0,0,0.6); padding:4px 10px; border-radius:20px; color:#a5b4fc; font-size:10px; font-family:monospace;'; | |||
coordsDiv.textContent = '📍 0, 0'; | |||
container.appendChild(toolbar); | |||
container.appendChild(viewport); | |||
container.appendChild(coordsDiv); | |||
container.appendChild(navDiv); | |||
function | // Renderizar camadas | ||
function renderizarCamadas() { | |||
camadasDiv.innerHTML = ''; | |||
camadas = []; | |||
marcadores = []; | |||
if (!config.layers || config.layers.length === 0) return; | |||
var | var andarExiste = false; | ||
for (var i = 0; i < config.layers.length; i++) { | |||
if (config.layers[i].id === andarAtual) andarExiste = true; | |||
} | |||
if (!andarExiste && config.layers.length > 0) andarAtual = config.layers[0].id; | |||
var | var andarInfo = null; | ||
for (var i = 0; i < config.layers.length; i++) { | |||
if (config.layers[i].id === andarAtual) andarInfo = config.layers[i]; | |||
} | |||
var nomeSpan = document.getElementById('floor-name-' + containerId); | |||
if (nomeSpan && andarInfo) nomeSpan.textContent = andarInfo.name; | |||
var | for (var i = 0; i < config.layers.length; i++) { | ||
var layer = config.layers[i]; | |||
var divLayer = document.createElement('div'); | |||
divLayer.className = 'mapa-layer'; | |||
divLayer.setAttribute('data-floor', layer.id); | |||
divLayer.style.display = layer.id === andarAtual ? 'block' : 'none'; | |||
divLayer.style.position = 'absolute'; | |||
divLayer.style.top = '0'; | |||
divLayer.style.left = '0'; | |||
divLayer.style.transform = 'translate(' + (layer.alignment?.offsetX || 0) + 'px, ' + (layer.alignment?.offsetY || 0) + 'px)'; | |||
divLayer.style.opacity = (layer.opacity || 100) / 100; | |||
var img = document.createElement('img'); | |||
img.style.display = 'block'; | |||
img.style.transform = 'scale(' + zoomAtual + ')'; | |||
img.style.transformOrigin = '0 0'; | |||
img.src = layer.imagePath || ''; | |||
img.onload = function() { camadasDiv.style.width = this.width + 'px'; camadasDiv.style.height = this.height + 'px'; }; | |||
img.onerror = function() { this.src = 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22800%22 height=%22600%22%3E%3Crect width=%22100%25%22 height=%22100%25%22 fill=%22%23334155%22/%3E%3Ctext x=%2250%25%22 y=%2250%25%22 text-anchor=%22middle%22 fill=%22white%22%3ESem imagem%3C/text%3E%3C/svg%3E'; }; | |||
divLayer.appendChild(img); | |||
var marcadoresDiv = document.createElement('div'); | |||
marcadoresDiv.style.position = 'absolute'; | |||
marcadoresDiv.style.top = '0'; | |||
marcadoresDiv.style.left = '0'; | |||
marcadoresDiv.style.width = '100%'; | |||
marcadoresDiv.style.height = '100%'; | |||
marcadoresDiv.style.pointerEvents = 'none'; | |||
if (layer.markers) { | |||
for (var j = 0; j < layer.markers.length; j++) { | |||
var marker = layer.markers[j]; | |||
var markerDiv = criarMarcador(marker, layer.id); | |||
marcadoresDiv.appendChild(markerDiv); | |||
marcadores.push({ el: markerDiv, data: marker }); | |||
} | } | ||
} | } | ||
divLayer.appendChild(marcadoresDiv); | |||
camadasDiv.appendChild(divLayer); | |||
camadas.push(divLayer); | |||
} | } | ||
} | |||
function criarMarcador(marker, floorId) { | |||
var div = document.createElement('div'); | |||
div.className = 'mapa-marker'; | |||
div.setAttribute('data-marker-id', marker.id); | |||
div.setAttribute('data-floor', floorId); | |||
div.setAttribute('data-x', marker.x); | |||
div.setAttribute('data-y', marker.y); | |||
div.style.position = 'absolute'; | |||
div.style.left = (marker.x * zoomAtual) + 'px'; | |||
div.style.top = (marker.y * zoomAtual) + 'px'; | |||
div.style.zIndex = '100'; | |||
div.style.transform = 'translate(-50%, -50%)'; | |||
div.style.cursor = (marker.action && marker.action !== 'none') ? 'pointer' : 'default'; | |||
var tamanhoIcone = Math.max(20, Math.floor(20 * zoomAtual)); | |||
var htmlIcone = marker.iconBase64 ? | |||
'<img src="' + marker.iconBase64 + '" style="width:' + tamanhoIcone + 'px; height:' + tamanhoIcone + 'px;">' : | |||
'<span style="font-size:' + tamanhoIcone + 'px;">📍</span>'; | |||
var textoAcao = ''; | |||
if (marker.action === 'popup') textoAcao = '📋 Informações'; | |||
else if (marker.action === 'nextFloor') textoAcao = '⬆️ Próximo andar'; | |||
else if (marker.action === 'prevFloor') textoAcao = '⬇️ Andar anterior'; | |||
else if (marker.action === 'gotoFloor') textoAcao = '🎯 Ir para andar'; | |||
else if (marker.action === 'link') textoAcao = '🔗 Link externo'; | |||
else textoAcao = '📍 Clique'; | |||
div.innerHTML = '<div style="display:flex; align-items:center; justify-content:center;">' + htmlIcone + '</div>' + | |||
'<div style="position:absolute; bottom:100%; left:50%; transform:translateX(-50%); background:#1e293b; color:white; padding:4px 8px; border-radius:6px; font-size:10px; white-space:nowrap; opacity:0; visibility:hidden; transition:0.2s; pointer-events:none;">' + | |||
'<strong>' + (marker.name || 'Marcador') + '</strong><br><small>' + textoAcao + '</small></div>' + | |||
'<div style="position:absolute; top:-8px; right:-8px; background:#ef4444; color:white; font-size:9px; min-width:16px; height:16px; border-radius:10px; display:flex; align-items:center; justify-content:center; padding:0 4px; ' + (!marker.hasBadge ? 'display:none;' : '') + '">' + (marker.hasBadge ? (marker.badgeNumber || marker.number || '') : '') + '</div>'; | |||
function | div.addEventListener('mouseenter', function() { | ||
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'; } | |||
}); | |||
function | if (marker.action && marker.action !== 'none') { | ||
div.addEventListener('click', function(e) { | |||
e.stopPropagation(); | |||
if (marker.action === 'popup') alert(marker.name + '\n\n' + (marker.actionData?.text || 'Sem informações')); | |||
else if (marker.action === 'nextFloor') irProximoAndar(); | |||
else if (marker.action === 'prevFloor') irAndarAnterior(); | |||
else if (marker.action === 'gotoFloor' && marker.actionData?.floorId !== undefined) irParaAndar(marker.actionData.floorId); | |||
else if (marker.action === 'link' && marker.actionData?.url && marker.actionData.url !== '#') window.open(marker.actionData.url, marker.actionData.target || '_blank'); | |||
}); | |||
} | } | ||
return div; | |||
} | |||
function atualizarPosicaoMarcadores() { | |||
for (var i = 0; i < marcadores.length; i++) { | |||
var item = marcadores[i]; | |||
if (parseInt(item.el.getAttribute('data-floor')) === andarAtual) { | |||
item.el.style.left = (item.data.x * zoomAtual) + 'px'; | |||
item.el.style.top = (item.data.y * zoomAtual) + 'px'; | |||
var ts = Math.max(20, Math.floor(20 * zoomAtual)); | |||
var idiv = item.el.children[0]; | |||
if (idiv) { | |||
if (item.data.iconBase64) idiv.innerHTML = '<img src="' + item.data.iconBase64 + '" style="width:' + ts + 'px; height:' + ts + 'px;">'; | |||
else idiv.innerHTML = '<span style="font-size:' + ts + 'px;">📍</span>'; | |||
} | } | ||
} | } | ||
} | } | ||
} | |||
function irProximoAndar() { | |||
var idx = -1; | |||
for (var i = 0; i < config.layers.length; i++) { | |||
if (config.layers[i].id === andarAtual) idx = i; | |||
} | |||
if (idx < config.layers.length - 1) irParaAndar(config.layers[idx + 1].id); | |||
} | |||
function irAndarAnterior() { | |||
var idx = -1; | |||
for (var i = 0; i < config.layers.length; i++) { | |||
if (config.layers[i].id === andarAtual) idx = i; | |||
} | |||
if (idx > 0) irParaAndar(config.layers[idx - 1].id); | |||
} | |||
function irParaAndar(floorId) { | |||
andarAtual = floorId; | |||
for (var i = 0; i < camadas.length; i++) { | |||
var lf = parseInt(camadas[i].getAttribute('data-floor')); | |||
camadas[i].style.display = lf === floorId ? 'block' : 'none'; | |||
} | |||
var fd = null; | |||
for (var i = 0; i < config.layers.length; i++) { | |||
if (config.layers[i].id === floorId) fd = config.layers[i]; | |||
} | |||
var nomeSpan = document.getElementById('floor-name-' + containerId); | |||
if (nomeSpan && fd) nomeSpan.textContent = fd.name; | |||
atualizarPosicaoMarcadores(); | |||
} | |||
function zoomIn() { | |||
if (zoomAtual < zoomMax) { | |||
var old = zoomAtual; | |||
zoomAtual = Math.min(zoomAtual + zoomPasso, zoomMax); | |||
aplicarZoom(old); | |||
} | } | ||
} | |||
function zoomOut() { | |||
if (zoomAtual > zoomMin) { | |||
var old = zoomAtual; | |||
zoomAtual = Math.max(zoomAtual - zoomPasso, zoomMin); | |||
aplicarZoom(old); | |||
} | } | ||
} | } | ||
-- | function aplicarZoom(oldZoom) { | ||
return | var imgs = document.querySelectorAll('.mapa-image'); | ||
for (var i = 0; i < imgs.length; i++) imgs[i].style.transform = 'scale(' + zoomAtual + ')'; | |||
var rect = viewport.getBoundingClientRect(); | |||
var cx = rect.width / 2, cy = rect.height / 2; | |||
var sx = (viewport.scrollLeft + cx) / oldZoom, sy = (viewport.scrollTop + cy) / oldZoom; | |||
viewport.scrollLeft = sx * zoomAtual - cx; | |||
viewport.scrollTop = sy * zoomAtual - cy; | |||
atualizarPosicaoMarcadores(); | |||
var zoomSpan = document.getElementById('zoom-level-' + containerId); | |||
if (zoomSpan) zoomSpan.textContent = Math.round(zoomAtual * 100) + '%'; | |||
} | |||
function resetarView() { | |||
zoomAtual = config.mapConfig.defaultZoom || 1; | |||
var imgs = document.querySelectorAll('.mapa-image'); | |||
for (var i = 0; i < imgs.length; i++) imgs[i].style.transform = 'scale(' + zoomAtual + ')'; | |||
viewport.scrollLeft = 0; | |||
viewport.scrollTop = 0; | |||
atualizarPosicaoMarcadores(); | |||
var zoomSpan = document.getElementById('zoom-level-' + containerId); | |||
if (zoomSpan) zoomSpan.textContent = Math.round(zoomAtual * 100) + '%'; | |||
} | |||
// Eventos dos botões | |||
document.getElementById('btn-zoom-in-' + containerId).addEventListener('click', zoomIn); | |||
document.getElementById('btn-zoom-out-' + containerId).addEventListener('click', zoomOut); | |||
document.getElementById('btn-reset-' + containerId).addEventListener('click', resetarView); | |||
document.getElementById('btn-prev-' + containerId).addEventListener('click', irAndarAnterior); | |||
document.getElementById('btn-next-' + containerId).addEventListener('click', irProximoAndar); | |||
// Arrastar mapa | |||
viewport.addEventListener('mousedown', function(e) { | |||
if (e.target.closest('.mapa-marker')) return; | |||
arrastando = true; | |||
arrasteInicio = { x: e.clientX, y: e.clientY, scrollLeft: viewport.scrollLeft, scrollTop: viewport.scrollTop }; | |||
viewport.style.cursor = 'grabbing'; | |||
e.preventDefault(); | |||
}); | |||
window.addEventListener('mousemove', function(e) { | |||
if (!arrastando) return; | |||
viewport.scrollLeft = arrasteInicio.scrollLeft - (e.clientX - arrasteInicio.x); | |||
viewport.scrollTop = arrasteInicio.scrollTop - (e.clientY - arrasteInicio.y); | |||
}); | |||
window.addEventListener('mouseup', function() { arrastando = false; viewport.style.cursor = 'grab'; }); | |||
// Zoom com scroll | |||
viewport.addEventListener('wheel', function(e) { | |||
if (e.ctrlKey) { e.preventDefault(); e.deltaY < 0 ? zoomIn() : zoomOut(); } | |||
}); | |||
// Coordenadas | |||
viewport.addEventListener('mousemove', function(e) { | |||
var rect = viewport.getBoundingClientRect(); | |||
var x = (e.clientX - rect.left + viewport.scrollLeft) / zoomAtual; | |||
var y = (e.clientY - rect.top + viewport.scrollTop) / zoomAtual; | |||
coordsDiv.textContent = '📍 ' + Math.round(x) + ', ' + Math.round(y) + ' | ' + Math.round(zoomAtual * 100) + '%'; | |||
}); | |||
renderizarCamadas(); | |||
} | |||
]] | |||
end | end | ||
return p | return p | ||
Edição das 19h48min de 9 de abril de 2026
A documentação para este módulo pode ser criada em Módulo:MapaJson/doc
local p = {}
function p.renderizar(frame)
-- Pegar os parâmetros
local args = frame.args
local mapaId = args.id or 'mapa1'
local largura = args.largura or '100%'
local altura = args.altura or '500px'
local titulo = args.titulo or 'Mapa'
local jsonFonte = args.json or ''
-- Se o JSON veio de uma página (ex: {{:Mapa:Config}}), buscar o conteúdo
if jsonFonte:match("^%{%{:[^}]+%}%}$") then
local nomePagina = jsonFonte:match("^%{%{:([^}]+)%}%}$")
if nomePagina then
local title = mw.title.new(nomePagina)
if title and title.exists then
jsonFonte = title:getContent() or ''
end
end
end
-- Limpar o JSON (remover quebras de linha e espaços extras)
jsonFonte = jsonFonte:gsub("\n", "")
jsonFonte = jsonFonte:gsub("\r", "")
jsonFonte = jsonFonte:gsub("\t", " ")
jsonFonte = jsonFonte:gsub(" +", " ")
-- Escapar para usar dentro de string JavaScript
local jsonEscapado = jsonFonte:gsub("'", "\\'")
-- Gerar o HTML com o visualizador
local html = [[
<div id="mapa-container-]] .. mapaId .. [[" style="width:]] .. largura .. [[; height:]] .. altura .. [[; background:#0f172a; border-radius:12px; overflow:hidden; position:relative;">
<div style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); color:#64748b; text-align:center;">
🗺️ Carregando mapa...<br>
<small>]] .. titulo .. [[</small>
</div>
</div>
<script>
(function() {
var containerId = 'mapa-container-]] .. mapaId .. [[';
var container = document.getElementById(containerId);
if (!container) return;
var jsonString = ']] .. jsonEscapado .. [[';
]] .. p.obterCodigoJS() .. [[
try {
var config = JSON.parse(jsonString);
if (config.layers && config.layers.length > 0) {
carregarMapa(containerId, config);
} else {
container.innerHTML = '<div style="padding:20px; text-align:center; color:#f59e0b;">⚠️ Nenhuma camada encontrada</div>';
}
} catch(e) {
container.innerHTML = '<div style="padding:20px; text-align:center; color:#ef4444;">❌ Erro ao carregar mapa: ' + e.message + '</div>';
}
})();
</script>]]
return html
end
function p.obterCodigoJS()
return [[
function carregarMapa(containerId, config) {
var container = document.getElementById(containerId);
if (!container) return;
container.innerHTML = '';
container.style.position = 'relative';
container.style.overflow = 'hidden';
// Zoom e navegação
var zoomAtual = config.mapConfig.defaultZoom || 1;
var andarAtual = config.mapConfig.initialFloor || 0;
var zoomMin = config.mapConfig.minZoom || 0.5;
var zoomMax = config.mapConfig.maxZoom || 3;
var zoomPasso = config.mapConfig.zoomStep || 0.1;
var camadas = [];
var marcadores = [];
var arrastando = false;
var arrasteInicio = { x: 0, y: 0, scrollLeft: 0, scrollTop: 0 };
// Criar toolbar
var toolbar = document.createElement('div');
toolbar.style.cssText = 'position:absolute; top:10px; left:10px; right:10px; z-index:100; display:flex; justify-content:space-between; gap:8px; flex-wrap:wrap;';
toolbar.innerHTML = '<div style="display:flex; gap:5px; background:rgba(0,0,0,0.7); padding:5px 10px; border-radius:30px;">' +
'<button id="btn-zoom-in-' + containerId + '" style="background:#334155; border:none; color:white; width:32px; height:32px; border-radius:50%; cursor:pointer;">+</button>' +
'<button id="btn-zoom-out-' + containerId + '" style="background:#334155; border:none; color:white; width:32px; height:32px; border-radius:50%; cursor:pointer;">-</button>' +
'<button id="btn-reset-' + containerId + '" style="background:#334155; border:none; color:white; width:32px; height:32px; border-radius:50%; cursor:pointer;">⟳</button>' +
'</div>' +
'<div style="background:rgba(0,0,0,0.7); padding:5px 15px; border-radius:30px; color:white; font-size:12px;"><span id="floor-name-' + containerId + '">' + (config.layers[0]?.name || 'Mapa') + '</span></div>' +
'<div id="zoom-level-' + containerId + '" style="background:rgba(0,0,0,0.7); padding:5px 12px; border-radius:30px; color:#a5b4fc; font-size:12px;">100%</div>';
// Viewport
var viewport = document.createElement('div');
viewport.style.cssText = 'width:100%; height:100%; overflow:auto; cursor:grab; background:#0f172a;';
var camadasDiv = document.createElement('div');
camadasDiv.style.cssText = 'position:relative; min-width:100%; min-height:100%;';
viewport.appendChild(camadasDiv);
// Navegação inferior
var navDiv = document.createElement('div');
navDiv.style.cssText = 'position:absolute; bottom:20px; left:20px; z-index:100; display:flex; gap:8px; background:rgba(0,0,0,0.7); padding:8px; border-radius:40px;';
navDiv.innerHTML = '<button id="btn-prev-' + containerId + '" style="background:#334155; border:none; color:white; width:36px; height:36px; border-radius:50%; cursor:pointer;">▲</button>' +
'<button id="btn-next-' + containerId + '" style="background:#334155; border:none; color:white; width:36px; height:36px; border-radius:50%; cursor:pointer;">▼</button>';
// Coordenadas
var coordsDiv = document.createElement('div');
coordsDiv.style.cssText = 'position:absolute; bottom:10px; right:10px; background:rgba(0,0,0,0.6); padding:4px 10px; border-radius:20px; color:#a5b4fc; font-size:10px; font-family:monospace;';
coordsDiv.textContent = '📍 0, 0';
container.appendChild(toolbar);
container.appendChild(viewport);
container.appendChild(coordsDiv);
container.appendChild(navDiv);
// Renderizar camadas
function renderizarCamadas() {
camadasDiv.innerHTML = '';
camadas = [];
marcadores = [];
if (!config.layers || config.layers.length === 0) return;
var andarExiste = false;
for (var i = 0; i < config.layers.length; i++) {
if (config.layers[i].id === andarAtual) andarExiste = true;
}
if (!andarExiste && config.layers.length > 0) andarAtual = config.layers[0].id;
var andarInfo = null;
for (var i = 0; i < config.layers.length; i++) {
if (config.layers[i].id === andarAtual) andarInfo = config.layers[i];
}
var nomeSpan = document.getElementById('floor-name-' + containerId);
if (nomeSpan && andarInfo) nomeSpan.textContent = andarInfo.name;
for (var i = 0; i < config.layers.length; i++) {
var layer = config.layers[i];
var divLayer = document.createElement('div');
divLayer.className = 'mapa-layer';
divLayer.setAttribute('data-floor', layer.id);
divLayer.style.display = layer.id === andarAtual ? 'block' : 'none';
divLayer.style.position = 'absolute';
divLayer.style.top = '0';
divLayer.style.left = '0';
divLayer.style.transform = 'translate(' + (layer.alignment?.offsetX || 0) + 'px, ' + (layer.alignment?.offsetY || 0) + 'px)';
divLayer.style.opacity = (layer.opacity || 100) / 100;
var img = document.createElement('img');
img.style.display = 'block';
img.style.transform = 'scale(' + zoomAtual + ')';
img.style.transformOrigin = '0 0';
img.src = layer.imagePath || '';
img.onload = function() { camadasDiv.style.width = this.width + 'px'; camadasDiv.style.height = this.height + 'px'; };
img.onerror = function() { this.src = 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22800%22 height=%22600%22%3E%3Crect width=%22100%25%22 height=%22100%25%22 fill=%22%23334155%22/%3E%3Ctext x=%2250%25%22 y=%2250%25%22 text-anchor=%22middle%22 fill=%22white%22%3ESem imagem%3C/text%3E%3C/svg%3E'; };
divLayer.appendChild(img);
var marcadoresDiv = document.createElement('div');
marcadoresDiv.style.position = 'absolute';
marcadoresDiv.style.top = '0';
marcadoresDiv.style.left = '0';
marcadoresDiv.style.width = '100%';
marcadoresDiv.style.height = '100%';
marcadoresDiv.style.pointerEvents = 'none';
if (layer.markers) {
for (var j = 0; j < layer.markers.length; j++) {
var marker = layer.markers[j];
var markerDiv = criarMarcador(marker, layer.id);
marcadoresDiv.appendChild(markerDiv);
marcadores.push({ el: markerDiv, data: marker });
}
}
divLayer.appendChild(marcadoresDiv);
camadasDiv.appendChild(divLayer);
camadas.push(divLayer);
}
}
function criarMarcador(marker, floorId) {
var div = document.createElement('div');
div.className = 'mapa-marker';
div.setAttribute('data-marker-id', marker.id);
div.setAttribute('data-floor', floorId);
div.setAttribute('data-x', marker.x);
div.setAttribute('data-y', marker.y);
div.style.position = 'absolute';
div.style.left = (marker.x * zoomAtual) + 'px';
div.style.top = (marker.y * zoomAtual) + 'px';
div.style.zIndex = '100';
div.style.transform = 'translate(-50%, -50%)';
div.style.cursor = (marker.action && marker.action !== 'none') ? 'pointer' : 'default';
var tamanhoIcone = Math.max(20, Math.floor(20 * zoomAtual));
var htmlIcone = marker.iconBase64 ?
'<img src="' + marker.iconBase64 + '" style="width:' + tamanhoIcone + 'px; height:' + tamanhoIcone + 'px;">' :
'<span style="font-size:' + tamanhoIcone + 'px;">📍</span>';
var textoAcao = '';
if (marker.action === 'popup') textoAcao = '📋 Informações';
else if (marker.action === 'nextFloor') textoAcao = '⬆️ Próximo andar';
else if (marker.action === 'prevFloor') textoAcao = '⬇️ Andar anterior';
else if (marker.action === 'gotoFloor') textoAcao = '🎯 Ir para andar';
else if (marker.action === 'link') textoAcao = '🔗 Link externo';
else textoAcao = '📍 Clique';
div.innerHTML = '<div style="display:flex; align-items:center; justify-content:center;">' + htmlIcone + '</div>' +
'<div style="position:absolute; bottom:100%; left:50%; transform:translateX(-50%); background:#1e293b; color:white; padding:4px 8px; border-radius:6px; font-size:10px; white-space:nowrap; opacity:0; visibility:hidden; transition:0.2s; pointer-events:none;">' +
'<strong>' + (marker.name || 'Marcador') + '</strong><br><small>' + textoAcao + '</small></div>' +
'<div style="position:absolute; top:-8px; right:-8px; background:#ef4444; color:white; font-size:9px; min-width:16px; height:16px; border-radius:10px; display:flex; align-items:center; justify-content:center; padding:0 4px; ' + (!marker.hasBadge ? 'display:none;' : '') + '">' + (marker.hasBadge ? (marker.badgeNumber || marker.number || '') : '') + '</div>';
div.addEventListener('mouseenter', function() {
this.style.transform = 'translate(-50%, -50%) scale(1.15)';
var tp = this.children[1];
if (tp) { tp.style.opacity = '1'; tp.style.visibility = 'visible'; }
});
div.addEventListener('mouseleave', function() {
this.style.transform = 'translate(-50%, -50%) scale(1)';
var tp = this.children[1];
if (tp) { tp.style.opacity = '0'; tp.style.visibility = 'hidden'; }
});
if (marker.action && marker.action !== 'none') {
div.addEventListener('click', function(e) {
e.stopPropagation();
if (marker.action === 'popup') alert(marker.name + '\n\n' + (marker.actionData?.text || 'Sem informações'));
else if (marker.action === 'nextFloor') irProximoAndar();
else if (marker.action === 'prevFloor') irAndarAnterior();
else if (marker.action === 'gotoFloor' && marker.actionData?.floorId !== undefined) irParaAndar(marker.actionData.floorId);
else if (marker.action === 'link' && marker.actionData?.url && marker.actionData.url !== '#') window.open(marker.actionData.url, marker.actionData.target || '_blank');
});
}
return div;
}
function atualizarPosicaoMarcadores() {
for (var i = 0; i < marcadores.length; i++) {
var item = marcadores[i];
if (parseInt(item.el.getAttribute('data-floor')) === andarAtual) {
item.el.style.left = (item.data.x * zoomAtual) + 'px';
item.el.style.top = (item.data.y * zoomAtual) + 'px';
var ts = Math.max(20, Math.floor(20 * zoomAtual));
var idiv = item.el.children[0];
if (idiv) {
if (item.data.iconBase64) idiv.innerHTML = '<img src="' + item.data.iconBase64 + '" style="width:' + ts + 'px; height:' + ts + 'px;">';
else idiv.innerHTML = '<span style="font-size:' + ts + 'px;">📍</span>';
}
}
}
}
function irProximoAndar() {
var idx = -1;
for (var i = 0; i < config.layers.length; i++) {
if (config.layers[i].id === andarAtual) idx = i;
}
if (idx < config.layers.length - 1) irParaAndar(config.layers[idx + 1].id);
}
function irAndarAnterior() {
var idx = -1;
for (var i = 0; i < config.layers.length; i++) {
if (config.layers[i].id === andarAtual) idx = i;
}
if (idx > 0) irParaAndar(config.layers[idx - 1].id);
}
function irParaAndar(floorId) {
andarAtual = floorId;
for (var i = 0; i < camadas.length; i++) {
var lf = parseInt(camadas[i].getAttribute('data-floor'));
camadas[i].style.display = lf === floorId ? 'block' : 'none';
}
var fd = null;
for (var i = 0; i < config.layers.length; i++) {
if (config.layers[i].id === floorId) fd = config.layers[i];
}
var nomeSpan = document.getElementById('floor-name-' + containerId);
if (nomeSpan && fd) nomeSpan.textContent = fd.name;
atualizarPosicaoMarcadores();
}
function zoomIn() {
if (zoomAtual < zoomMax) {
var old = zoomAtual;
zoomAtual = Math.min(zoomAtual + zoomPasso, zoomMax);
aplicarZoom(old);
}
}
function zoomOut() {
if (zoomAtual > zoomMin) {
var old = zoomAtual;
zoomAtual = Math.max(zoomAtual - zoomPasso, zoomMin);
aplicarZoom(old);
}
}
function aplicarZoom(oldZoom) {
var imgs = document.querySelectorAll('.mapa-image');
for (var i = 0; i < imgs.length; i++) imgs[i].style.transform = 'scale(' + zoomAtual + ')';
var rect = viewport.getBoundingClientRect();
var cx = rect.width / 2, cy = rect.height / 2;
var sx = (viewport.scrollLeft + cx) / oldZoom, sy = (viewport.scrollTop + cy) / oldZoom;
viewport.scrollLeft = sx * zoomAtual - cx;
viewport.scrollTop = sy * zoomAtual - cy;
atualizarPosicaoMarcadores();
var zoomSpan = document.getElementById('zoom-level-' + containerId);
if (zoomSpan) zoomSpan.textContent = Math.round(zoomAtual * 100) + '%';
}
function resetarView() {
zoomAtual = config.mapConfig.defaultZoom || 1;
var imgs = document.querySelectorAll('.mapa-image');
for (var i = 0; i < imgs.length; i++) imgs[i].style.transform = 'scale(' + zoomAtual + ')';
viewport.scrollLeft = 0;
viewport.scrollTop = 0;
atualizarPosicaoMarcadores();
var zoomSpan = document.getElementById('zoom-level-' + containerId);
if (zoomSpan) zoomSpan.textContent = Math.round(zoomAtual * 100) + '%';
}
// Eventos dos botões
document.getElementById('btn-zoom-in-' + containerId).addEventListener('click', zoomIn);
document.getElementById('btn-zoom-out-' + containerId).addEventListener('click', zoomOut);
document.getElementById('btn-reset-' + containerId).addEventListener('click', resetarView);
document.getElementById('btn-prev-' + containerId).addEventListener('click', irAndarAnterior);
document.getElementById('btn-next-' + containerId).addEventListener('click', irProximoAndar);
// Arrastar mapa
viewport.addEventListener('mousedown', function(e) {
if (e.target.closest('.mapa-marker')) return;
arrastando = true;
arrasteInicio = { x: e.clientX, y: e.clientY, scrollLeft: viewport.scrollLeft, scrollTop: viewport.scrollTop };
viewport.style.cursor = 'grabbing';
e.preventDefault();
});
window.addEventListener('mousemove', function(e) {
if (!arrastando) return;
viewport.scrollLeft = arrasteInicio.scrollLeft - (e.clientX - arrasteInicio.x);
viewport.scrollTop = arrasteInicio.scrollTop - (e.clientY - arrasteInicio.y);
});
window.addEventListener('mouseup', function() { arrastando = false; viewport.style.cursor = 'grab'; });
// Zoom com scroll
viewport.addEventListener('wheel', function(e) {
if (e.ctrlKey) { e.preventDefault(); e.deltaY < 0 ? zoomIn() : zoomOut(); }
});
// Coordenadas
viewport.addEventListener('mousemove', function(e) {
var rect = viewport.getBoundingClientRect();
var x = (e.clientX - rect.left + viewport.scrollLeft) / zoomAtual;
var y = (e.clientY - rect.top + viewport.scrollTop) / zoomAtual;
coordsDiv.textContent = '📍 ' + Math.round(x) + ', ' + Math.round(y) + ' | ' + Math.round(zoomAtual * 100) + '%';
});
renderizarCamadas();
}
]]
end
return p