import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react' import { apiUrl, getAuthToken } from '../api' import LeafletMapFrame from './LeafletMapFrame' function hashHtml(value) { let hash = 0 const step = Math.max(1, Math.floor(value.length / 64)) for (let i = 0; i < value.length; i += step) { hash = ((hash << 5) - hash) + value.charCodeAt(i) hash |= 0 } return `${value.length}-${Math.abs(hash)}` } const MapFrame = forwardRef(function MapFrame({ html, payload = null, sessionId = '' }, ref) { const shellRef = useRef(null) const iframeRef = useRef(null) const leafletRef = useRef(null) const timersRef = useRef([]) const [isFullscreen, setIsFullscreen] = useState(false) const frameKey = useMemo(() => hashHtml(html || ''), [html]) const clearTimers = useCallback(() => { timersRef.current.forEach((id) => window.clearTimeout(id)) timersRef.current = [] }, []) const recenterFromLayers = useCallback(() => { const iframe = iframeRef.current if (!iframe) return try { const win = iframe.contentWindow const doc = iframe.contentDocument || win?.document if (!win || !doc || !win.L) return const maps = Object.values(win).filter( (item) => item && typeof item.fitBounds === 'function' && typeof item.eachLayer === 'function' && typeof item.invalidateSize === 'function', ) const map = maps[0] if (!map) return map.invalidateSize(true) const dataBounds = win.L.latLngBounds([]) const visitLayerTree = (layer, visited, fn) => { if (!layer || !layer._leaflet_id || visited.has(layer._leaflet_id)) return visited.add(layer._leaflet_id) fn(layer) if (typeof layer.eachLayer === 'function') { layer.eachLayer((child) => { visitLayerTree(child, visited, fn) }) } } const isBairroLayer = (layer) => { try { const nome = String(layer?.options?.name || layer?.layerName || '').toLowerCase() if (nome.includes('bairro')) return true const featureName = String(layer?.feature?.properties?.NOME || layer?.feature?.properties?.BAIRRO || '').toLowerCase() return featureName.length > 0 } catch { return false } } const shouldIgnoreForBounds = (layer) => { try { return Boolean(layer?.options?.mesaIgnoreBounds) } catch { return false } } const visited = new Set() map.eachLayer((layer) => { visitLayerTree(layer, visited, (item) => { try { if (item instanceof win.L.TileLayer) return if (isBairroLayer(item)) return if (shouldIgnoreForBounds(item)) return if (item instanceof win.L.CircleMarker || item instanceof win.L.Marker) { const latlng = item.getLatLng() if (latlng && Number.isFinite(latlng.lat) && Number.isFinite(latlng.lng)) { dataBounds.extend(latlng) } return } const isContainerLayer = typeof item.eachLayer === 'function' if (!isContainerLayer && typeof item.getBounds === 'function') { const itemBounds = item.getBounds() if (itemBounds && typeof itemBounds.isValid === 'function' && itemBounds.isValid()) { dataBounds.extend(itemBounds) } } } catch { // no-op } }) }) if (dataBounds.isValid()) { const size = map.getSize ? map.getSize() : null const basePadding = size ? Math.max(34, Math.min(84, Math.round(Math.min(size.x, size.y) * 0.085))) : 48 map.fitBounds(dataBounds, { padding: [basePadding, basePadding], maxZoom: 18, animate: false }) } } catch { // no-op } }, []) const scheduleRecenter = useCallback(() => { clearTimers() ;[40, 180, 520, 1100].forEach((delay) => { const timerId = window.setTimeout(() => { recenterFromLayers() }, delay) timersRef.current.push(timerId) }) }, [clearTimers, recenterFromLayers]) const refreshMapSize = useCallback(() => { if (leafletRef.current?.fitToPayloadBounds) { window.setTimeout(() => { leafletRef.current?.fitToPayloadBounds?.(48) }, 120) return } scheduleRecenter() }, [scheduleRecenter]) const compartilharContextoComIframe = useCallback(() => { const frameWindow = iframeRef.current?.contentWindow if (!frameWindow) return try { frameWindow.__MESA_MAP_CONTEXT__ = { apiBase: apiUrl(''), authToken: getAuthToken(), } } catch { // O iframe ainda pode usar o endpoint relativo na versão publicada. } }, []) const toggleFullscreen = useCallback(async (event) => { event.preventDefault() event.stopPropagation() const shell = shellRef.current if (!shell) return try { if (document.fullscreenElement === shell) { await document.exitFullscreen() } else if (typeof shell.requestFullscreen === 'function') { await shell.requestFullscreen() } } catch { // Navegadores podem bloquear fullscreen fora de interação direta. } }, []) useEffect(() => { return () => { clearTimers() } }, [clearTimers]) useEffect(() => { function onFullscreenChange() { const active = document.fullscreenElement === shellRef.current setIsFullscreen(active) refreshMapSize() } document.addEventListener('fullscreenchange', onFullscreenChange) return () => document.removeEventListener('fullscreenchange', onFullscreenChange) }, [refreshMapSize]) useImperativeHandle(ref, () => ({ fitToPayloadBounds(padding = 48) { if (leafletRef.current?.fitToPayloadBounds) { return leafletRef.current.fitToPayloadBounds(padding) } return false }, async downloadSelectionPng(selectionRect, fileName = 'mapa-recorte.png') { if (leafletRef.current?.downloadSelectionPng) { return leafletRef.current.downloadSelectionPng(selectionRect, fileName) } throw new Error('A exportacao em PNG esta disponivel apenas para mapas interativos.') }, async downloadVisiblePng(fileName = 'mapa.png') { if (leafletRef.current?.downloadVisiblePng) { return leafletRef.current.downloadVisiblePng(fileName) } throw new Error('A exportacao em PNG esta disponivel apenas para mapas interativos.') }, }), []) if (!(payload && payload.type === 'mesa_leaflet_payload') && !html) { return