Widget:MapViewer.js

De Wiki Gla
Revisão de 02h07min de 8 de abril de 2026 por Ceu azul (discussão | contribs)
Ir para navegação Ir para pesquisar

<script>

 (function() {
 console.log('carregou mapviewer');
   // Verificar se já foi carregado
   if (window.MWMapViewerLoaded) return;
   window.MWMapViewerLoaded = true;
   
   // Estilos do widget
   const styles = `
       <style>
           .mw-map-viewer {
               position: relative;
               background: #0f172a;
               border-radius: 12px;
               overflow: hidden;
               box-shadow: 0 4px 20px rgba(0,0,0,0.2);
               font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
           }
           .mw-map-viewer .map-viewport {
               width: 100%;
               height: 100%;
               overflow: auto;
               cursor: grab;
               background: #0f172a;
           }
           .mw-map-viewer .map-viewport:active {
               cursor: grabbing;
           }
           .mw-map-viewer .map-layers {
               position: relative;
               min-width: 100%;
               min-height: 100%;
           }
           .mw-map-viewer .map-layer {
               position: absolute;
               top: 0;
               left: 0;
               transform-origin: 0 0;
           }
           .mw-map-viewer .map-image {
               display: block;
               transform-origin: 0 0;
               pointer-events: none;
               border-radius: 4px;
           }
           .mw-map-viewer .map-marker {
               position: absolute;
               cursor: pointer;
               z-index: 100;
               transform: translate(-50%, -50%);
               transition: transform 0.1s ease;
           }
           .mw-map-viewer .map-marker:hover {
               transform: translate(-50%, -50%) scale(1.15);
               z-index: 102;
           }
           .mw-map-viewer .marker-icon {
               display: flex;
               align-items: center;
               justify-content: center;
               filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));
           }
           .mw-map-viewer .marker-tooltip {
               position: absolute;
               bottom: 100%;
               left: 50%;
               transform: translateX(-50%) translateY(-8px);
               background: #1e293b;
               color: #e2e8f0;
               padding: 4px 10px;
               border-radius: 8px;
               font-size: 11px;
               white-space: nowrap;
               opacity: 0;
               visibility: hidden;
               transition: all 0.2s;
               pointer-events: none;
               box-shadow: 0 2px 8px rgba(0,0,0,0.2);
               border: 1px solid #334155;
           }
           .mw-map-viewer .map-marker:hover .marker-tooltip {
               opacity: 1;
               visibility: visible;
               transform: translateX(-50%) translateY(-12px);
           }
           .mw-map-viewer .marker-badge {
               position: absolute;
               top: -8px;
               right: -8px;
               background: #ef4444;
               color: white;
               font-size: 9px;
               font-weight: bold;
               min-width: 16px;
               height: 16px;
               border-radius: 10px;
               display: flex;
               align-items: center;
               justify-content: center;
               padding: 0 4px;
               font-family: monospace;
               box-shadow: 0 1px 3px rgba(0,0,0,0.2);
           }
           .mw-map-viewer .marker-badge.hidden {
               display: none;
           }
           .mw-map-viewer .viewer-toolbar {
               position: absolute;
               top: 12px;
               left: 12px;
               right: 12px;
               display: flex;
               justify-content: space-between;
               z-index: 101;
               gap: 10px;
               flex-wrap: wrap;
           }
           .mw-map-viewer .viewer-controls {
               display: flex;
               gap: 5px;
               background: rgba(0,0,0,0.6);
               backdrop-filter: blur(8px);
               padding: 5px 10px;
               border-radius: 40px;
           }
           .mw-map-viewer .viewer-controls button {
               background: #334155;
               border: none;
               color: white;
               width: 32px;
               height: 32px;
               border-radius: 50%;
               cursor: pointer;
               font-size: 16px;
               transition: all 0.2s;
           }
           .mw-map-viewer .viewer-controls button:hover {
               background: #6366f1;
               transform: scale(1.05);
           }
           .mw-map-viewer .floor-info {
               background: rgba(0,0,0,0.6);
               backdrop-filter: blur(8px);
               padding: 5px 15px;
               border-radius: 40px;
               color: white;
               font-size: 12px;
           }
           .mw-map-viewer .zoom-level {
               background: rgba(0,0,0,0.6);
               backdrop-filter: blur(8px);
               padding: 5px 12px;
               border-radius: 40px;
               color: #a5b4fc;
               font-size: 12px;
               font-family: monospace;
           }
           .mw-map-viewer .coordinates-panel {
               position: absolute;
               bottom: 10px;
               right: 10px;
               background: rgba(0,0,0,0.6);
               backdrop-filter: blur(8px);
               padding: 4px 10px;
               border-radius: 20px;
               color: #a5b4fc;
               font-size: 10px;
               font-family: monospace;
               z-index: 100;
               pointer-events: none;
           }
           .mw-map-viewer .nav-controls {
               position: absolute;
               bottom: 20px;
               left: 20px;
               display: flex;
               gap: 8px;
               z-index: 101;
           }
           .mw-map-viewer .nav-controls button {
               background: rgba(0,0,0,0.7);
               backdrop-filter: blur(8px);
               border: none;
               color: white;
               width: 40px;
               height: 40px;
               border-radius: 50%;
               cursor: pointer;
               font-size: 18px;
               transition: all 0.2s;
           }
           .mw-map-viewer .nav-controls button:hover {
               background: #6366f1;
               transform: scale(1.05);
           }
           .mw-map-viewer-popup {
               display: none;
               position: fixed;
               z-index: 10000;
               left: 0;
               top: 0;
               width: 100%;
               height: 100%;
               background: rgba(0,0,0,0.7);
               justify-content: center;
               align-items: center;
           }
           .mw-map-viewer-popup .popup-content {
               background: #1e293b;
               border-radius: 16px;
               max-width: 350px;
               width: 90%;
               padding: 20px;
               position: relative;
               border: 1px solid #334155;
               animation: popupSlide 0.3s ease;
           }
           @keyframes popupSlide {
               from { transform: translateY(-30px); opacity: 0; }
               to { transform: translateY(0); opacity: 1; }
           }
           .mw-map-viewer-popup .popup-close {
               position: absolute;
               right: 15px;
               top: 10px;
               font-size: 24px;
               cursor: pointer;
               color: #94a3b8;
           }
           .mw-map-viewer-popup .popup-close:hover {
               color: #ef4444;
           }
           .mw-map-viewer-popup h3 {
               color: #a5b4fc;
               margin-bottom: 12px;
           }
           .mw-map-viewer-popup p {
               color: #cbd5e1;
               line-height: 1.5;
               font-size: 14px;
           }
           .mw-map-viewer-popup img {
               max-width: 100%;
               border-radius: 12px;
               margin-bottom: 16px;
           }
           @media (max-width: 768px) {
               .mw-map-viewer .viewer-controls button {
                   width: 28px;
                   height: 28px;
                   font-size: 14px;
               }
               .mw-map-viewer .floor-info {
                   font-size: 10px;
                   padding: 4px 12px;
               }
               .mw-map-viewer .zoom-level {
                   font-size: 10px;
                   padding: 4px 10px;
               }
           }
       </style>
   `;
   
   class MWMapViewer {
       constructor(container, config, options = {}) {
           this.container = container;
           this.config = this.normalizeConfig(config);
           this.width = options.width || '100%';
           this.height = options.height || '500px';
           
           this.currentZoom = this.config.mapConfig?.defaultZoom || 1;
           this.currentFloor = this.config.mapConfig?.initialFloor || 0;
           this.minZoom = this.config.mapConfig?.minZoom || 0.5;
           this.maxZoom = this.config.mapConfig?.maxZoom || 3;
           this.zoomStep = this.config.mapConfig?.zoomStep || 0.1;
           
           this.layers = [];
           this.markers = [];
           this.isPanning = false;
           this.panStart = { x: 0, y: 0, scrollLeft: 0, scrollTop: 0 };
           
           this.init();
       }
       
       normalizeConfig(config) {
           if (!config.mapConfig) {
               config.mapConfig = { initialFloor: 0, defaultZoom: 1, minZoom: 0.5, maxZoom: 3, zoomStep: 0.1 };
           }
           if (!config.layers) config.layers = [];
           
           config.layers.forEach(layer => {
               if (!layer.alignment) layer.alignment = { offsetX: 0, offsetY: 0, scale: 1 };
               if (layer.opacity === undefined) layer.opacity = 100;
               if (!layer.markers) layer.markers = [];
               
               layer.markers.forEach(marker => {
                   if (!marker.actionData) marker.actionData = {};
                   if (marker.action === 'gotoFloor' && marker.actionData.floorId === undefined) marker.actionData.floorId = 0;
                   if (marker.action === 'link' && !marker.actionData.url) marker.actionData.url = '#';
                   if (marker.action === 'popup' && !marker.actionData.text) marker.actionData.text = 'Sem informações';
               });
           });
           return config;
       }
       
       init() {
           this.createDOM();
           this.renderLayers();
           this.setupEvents();
           this.updateMarkersPosition();
           this.initPopup();
       }
       
       createDOM() {
           this.container.innerHTML = ;
           this.container.className = 'mw-map-viewer';
           this.container.style.width = this.width;
           this.container.style.height = this.height;
           this.container.style.position = 'relative';
           this.container.style.overflow = 'hidden';
           
           this.container.innerHTML = `
                       <button class="mw-zoom-in" title="Zoom In (+ / Ctrl+Scroll)">+</button>
                       <button class="mw-zoom-out" title="Zoom Out (- / Ctrl+Scroll)">-</button>
                       <button class="mw-reset" title="Resetar Visualização (R)">⟳</button>
                       Carregando...
                       100%
                   📍 0, 0
           `;
           
           this.viewport = this.container.querySelector('.map-viewport');
           this.layersContainer = this.container.querySelector('.map-layers');
           this.floorNameSpan = this.container.querySelector('.mw-floor-name');
           this.zoomLevelSpan = this.container.querySelector('.mw-zoom-level');
           this.coordsSpan = this.container.querySelector('.mw-coords');
           
           // Bind events
           this.container.querySelector('.mw-zoom-in')?.addEventListener('click', () => this.zoomIn());
           this.container.querySelector('.mw-zoom-out')?.addEventListener('click', () => this.zoomOut());
           this.container.querySelector('.mw-reset')?.addEventListener('click', () => this.resetView());
           this.container.querySelector('.mw-prev-floor')?.addEventListener('click', () => this.prevFloor());
           this.container.querySelector('.mw-next-floor')?.addEventListener('click', () => this.nextFloor());
           
           this.setupInteractions();
           this.setupKeyboard();
       }
       
       setupInteractions() {
           // Pan
           this.viewport.addEventListener('mousedown', (e) => {
               if (e.target.closest('.map-marker')) return;
               this.isPanning = true;
               this.panStart = {
                   x: e.clientX,
                   y: e.clientY,
                   scrollLeft: this.viewport.scrollLeft,
                   scrollTop: this.viewport.scrollTop
               };
               this.viewport.style.cursor = 'grabbing';
               e.preventDefault();
           });
           
           window.addEventListener('mousemove', (e) => {
               if (!this.isPanning) return;
               this.viewport.scrollLeft = this.panStart.scrollLeft - (e.clientX - this.panStart.x);
               this.viewport.scrollTop = this.panStart.scrollTop - (e.clientY - this.panStart.y);
           });
           
           window.addEventListener('mouseup', () => {
               this.isPanning = false;
               this.viewport.style.cursor = 'grab';
           });
           
           // Zoom com scroll
           this.viewport.addEventListener('wheel', (e) => {
               if (e.ctrlKey) {
                   e.preventDefault();
                   e.deltaY < 0 ? this.zoomIn() : this.zoomOut();
               }
           });
           
           // Coordenadas
           this.viewport.addEventListener('mousemove', (e) => {
               const rect = this.viewport.getBoundingClientRect();
               const x = (e.clientX - rect.left + this.viewport.scrollLeft) / this.currentZoom;
               const y = (e.clientY - rect.top + this.viewport.scrollTop) / this.currentZoom;
               if (this.coordsSpan) {
                   this.coordsSpan.textContent = `📍 ${Math.round(x)}, ${Math.round(y)} | ${Math.round(this.currentZoom * 100)}%`;
               }
           });
       }
       
       setupKeyboard() {
           const handler = (e) => {
               if (e.target.tagName === 'INPUT') return;
               switch(e.key) {
                   case '+': case '=': e.preventDefault(); this.zoomIn(); break;
                   case '-': case '_': e.preventDefault(); this.zoomOut(); break;
                   case 'r': case 'R': e.preventDefault(); this.resetView(); break;
                   case 'ArrowUp': case 'PageUp': e.preventDefault(); this.prevFloor(); break;
                   case 'ArrowDown': case 'PageDown': e.preventDefault(); this.nextFloor(); break;
               }
           };
           document.addEventListener('keydown', handler);
       }
       
       renderLayers() {
           if (!this.layersContainer) return;
           this.layersContainer.innerHTML = ;
           this.layers = [];
           this.markers = [];
           
           if (!this.config.layers || this.config.layers.length === 0) {

this.layersContainer.innerHTML = '

Nenhuma camada carregada

';

               return;
           }
           
           if (!this.config.layers.find(l => l.id === this.currentFloor)) {
               this.currentFloor = this.config.layers[0]?.id || 0;
           }
           
           const currentFloorData = this.config.layers.find(l => l.id === this.currentFloor);
           if (currentFloorData && this.floorNameSpan) {
               this.floorNameSpan.textContent = currentFloorData.name;
           }
           
           this.config.layers.forEach((layer) => {
               const $layer = document.createElement('div');
               $layer.className = 'map-layer';
               $layer.setAttribute('data-floor', layer.id);
               $layer.style.display = layer.id === this.currentFloor ? 'block' : 'none';
               $layer.style.transform = `translate(${layer.alignment?.offsetX || 0}px, ${layer.alignment?.offsetY || 0}px)`;
               $layer.style.opacity = (layer.opacity || 100) / 100;
               
               const $img = document.createElement('img');
               $img.className = 'map-image';
               $img.style.transform = `scale(${this.currentZoom})`;
               $img.style.transformOrigin = '0 0';
               
               if (layer.imagePath) {
                   $img.src = layer.imagePath;
               } else {
                   const canvas = document.createElement('canvas');
                   canvas.width = 800;
                   canvas.height = 600;
                   const ctx = canvas.getContext('2d');
                   ctx.fillStyle = '#334155';
                   ctx.fillRect(0, 0, canvas.width, canvas.height);
                   ctx.fillStyle = 'white';
                   ctx.font = '20px Arial';
                   ctx.fillText(layer.name || `Andar ${layer.id + 1}`, 50, 100);
                   $img.src = canvas.toDataURL();
               }
               
               $img.onload = () => {
                   this.layersContainer.style.width = `${$img.width}px`;
                   this.layersContainer.style.height = `${$img.height}px`;
               };
               
               $layer.appendChild($img);
               
               const $markersContainer = document.createElement('div');
               $markersContainer.style.position = 'absolute';
               $markersContainer.style.top = '0';
               $markersContainer.style.left = '0';
               $markersContainer.style.width = '100%';
               $markersContainer.style.height = '100%';
               $markersContainer.style.pointerEvents = 'none';
               
               if (layer.markers && layer.markers.length > 0) {
                   layer.markers.forEach(marker => {
                       const $marker = this.createMarker(marker, layer.id);
                       $markersContainer.appendChild($marker);
                       this.markers.push({ element: $marker, data: marker });
                   });
               }
               
               $layer.appendChild($markersContainer);
               this.layersContainer.appendChild($layer);
               this.layers.push($layer);
           });
       }
       
       createMarker(marker, floorId) {
           const $marker = document.createElement('div');
           $marker.className = 'map-marker';
           $marker.setAttribute('data-marker-id', marker.id);
           $marker.setAttribute('data-floor', floorId);
           $marker.setAttribute('data-x', marker.x);
           $marker.setAttribute('data-y', marker.y);
           
           $marker.style.position = 'absolute';
           $marker.style.left = `${marker.x * this.currentZoom}px`;
           $marker.style.top = `${marker.y * this.currentZoom}px`;
           $marker.style.zIndex = '100';
           $marker.style.transform = 'translate(-50%, -50%)';
           
           const hasAction = marker.action && marker.action !== 'none';
           $marker.style.cursor = hasAction ? 'pointer' : 'default';
           
           const iconSize = Math.max(20, Math.floor(20 * this.currentZoom));
           let iconHtml = marker.iconBase64 ? 
               `<img src="${marker.iconBase64}" style="width:${iconSize}px; height:${iconSize}px;">` :
               `📍`;
           
           let actionText = ;
           switch(marker.action) {
               case 'popup': actionText = '📋 Informações'; break;
               case 'nextFloor': actionText = '⬆️ Próximo andar'; break;
               case 'prevFloor': actionText = '⬇️ Andar anterior'; break;
               case 'gotoFloor': actionText = '🎯 Ir para andar'; break;
               case 'link': actionText = '🔗 Link externo'; break;
               default: actionText = '📍 Clique';
           }
           
           $marker.innerHTML = `
${iconHtml}
                   ${marker.name || 'Marcador'}
${actionText}
${marker.hasBadge ? (marker.badgeNumber || marker.number || ) : }
           `;
           
           // Hover effects
           $marker.addEventListener('mouseenter', () => {
               $marker.style.transform = 'translate(-50%, -50%) scale(1.15)';
           });
           $marker.addEventListener('mouseleave', () => {
               $marker.style.transform = 'translate(-50%, -50%) scale(1)';
           });
           
           // Clique action
           if (hasAction) {
               $marker.addEventListener('click', (e) => {
                   e.stopPropagation();
                   this.executeAction(marker);
               });
           }
           
           return $marker;
       }
       
       executeAction(marker) {
           switch(marker.action) {
               case 'popup':
                   this.showPopup(marker);
                   break;
               case 'nextFloor':
                   this.nextFloor();
                   break;
               case 'prevFloor':
                   this.prevFloor();
                   break;
               case 'gotoFloor':
                   if (marker.actionData?.floorId !== undefined) {
                       this.goToFloor(marker.actionData.floorId);
                   }
                   break;
               case 'link':
                   if (marker.actionData?.url && marker.actionData.url !== '#') {
                       window.open(marker.actionData.url, marker.actionData.target || '_blank');
                   }
                   break;
           }
       }
       
       initPopup() {
           if (document.querySelector('.mw-map-viewer-popup')) return;
           
           const popupHtml = `
           `;
           document.body.insertAdjacentHTML('beforeend', popupHtml);
           
           this.popup = document.querySelector('.mw-map-viewer-popup');
           this.popupBody = this.popup?.querySelector('.popup-body');
           
           this.popup?.querySelector('.popup-close')?.addEventListener('click', () => this.closePopup());
           this.popup?.addEventListener('click', (e) => {
               if (e.target === this.popup) this.closePopup();
           });
       }
       
       showPopup(marker) {
           if (!this.popup) this.initPopup();
           
           const iconHtml = marker.iconBase64 ?
               `<img src="${marker.iconBase64}" style="width:32px; height:32px; vertical-align:middle; margin-right:8px;">` :
               `📍`;
           
           this.popupBody.innerHTML = `
               ${marker.actionData?.image ? `<img src="${marker.actionData.image}" alt="${marker.name}">` : }
                   ${iconHtml}

${marker.name}

${marker.actionData?.text || 'Sem informações'}

           `;
           
           this.popup.style.display = 'flex';
       }
       
       closePopup() {
           if (this.popup) this.popup.style.display = 'none';
       }
       
       updateMarkersPosition() {
           this.markers.forEach(({ element, data }) => {
               if (parseInt(element.getAttribute('data-floor')) === this.currentFloor) {
                   element.style.left = `${data.x * this.currentZoom}px`;
                   element.style.top = `${data.y * this.currentZoom}px`;
                   
                   const iconSize = Math.max(20, Math.floor(20 * this.currentZoom));
                   const iconDiv = element.querySelector('.marker-icon');
                   if (iconDiv) {
                       if (data.iconBase64) {
                           iconDiv.innerHTML = `<img src="${data.iconBase64}" style="width:${iconSize}px; height:${iconSize}px;">`;
                       } else {
                           iconDiv.innerHTML = `📍`;
                       }
                   }
               }
           });
       }
       
       zoomIn() {
           if (this.currentZoom < this.maxZoom) {
               const oldZoom = this.currentZoom;
               this.currentZoom = Math.min(this.currentZoom + this.zoomStep, this.maxZoom);
               this.applyZoom(oldZoom);
           }
       }
       
       zoomOut() {
           if (this.currentZoom > this.minZoom) {
               const oldZoom = this.currentZoom;
               this.currentZoom = Math.max(this.currentZoom - this.zoomStep, this.minZoom);
               this.applyZoom(oldZoom);
           }
       }
       
       applyZoom(oldZoom) {
           this.container.querySelectorAll('.map-image').forEach(img => {
               img.style.transform = `scale(${this.currentZoom})`;
           });
           
           const rect = this.viewport.getBoundingClientRect();
           const cx = rect.width / 2, cy = rect.height / 2;
           const sx = (this.viewport.scrollLeft + cx) / oldZoom;
           const sy = (this.viewport.scrollTop + cy) / oldZoom;
           
           this.viewport.scrollLeft = sx * this.currentZoom - cx;
           this.viewport.scrollTop = sy * this.currentZoom - cy;
           
           this.updateMarkersPosition();
           if (this.zoomLevelSpan) {
               this.zoomLevelSpan.textContent = `${Math.round(this.currentZoom * 100)}%`;
           }
       }
       
       resetView() {
           this.currentZoom = this.config.mapConfig?.defaultZoom || 1;
           this.container.querySelectorAll('.map-image').forEach(img => {
               img.style.transform = `scale(${this.currentZoom})`;
           });
           this.viewport.scrollLeft = 0;
           this.viewport.scrollTop = 0;
           this.updateMarkersPosition();
           if (this.zoomLevelSpan) {
               this.zoomLevelSpan.textContent = `${Math.round(this.currentZoom * 100)}%`;
           }
       }
       
       nextFloor() {
           const idx = this.config.layers.findIndex(l => l.id === this.currentFloor);
           if (idx < this.config.layers.length - 1) {
               this.goToFloor(this.config.layers[idx + 1].id);
           }
       }
       
       prevFloor() {
           const idx = this.config.layers.findIndex(l => l.id === this.currentFloor);
           if (idx > 0) {
               this.goToFloor(this.config.layers[idx - 1].id);
           }
       }
       
       goToFloor(floorId) {
           this.currentFloor = floorId;
           
           this.layers.forEach(layer => {
               const layerFloor = parseInt(layer.getAttribute('data-floor'));
               layer.style.display = layerFloor === floorId ? 'block' : 'none';
           });
           
           const floorData = this.config.layers.find(l => l.id === floorId);
           if (floorData && this.floorNameSpan) {
               this.floorNameSpan.textContent = floorData.name;
           }
           
           setTimeout(() => this.updateMarkersPosition(), 50);
       }
       
       // Método para atualizar configuração dinamicamente
       updateConfig(config) {
           this.config = this.normalizeConfig(config);
           this.renderLayers();
           this.updateMarkersPosition();
       }
   }
   
   // Registrar widget globalmente
   window.MWMapViewer = MWMapViewer;
   window.MWMapViewerStyles = styles;

})(); </script>