mesa-react / frontend /src /components /MapFrame.jsx
Guilherme Silberfarb Costa
correcao de trabalhos tecnicos, layer inundacao e bugs
9ca4757
Raw
History Blame Contribute Delete
7.63 kB
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 <div className="empty-box">Mapa indisponivel.</div>
}
const mapContent = payload && payload.type === 'mesa_leaflet_payload'
? (
<LeafletMapFrame
ref={leafletRef}
payload={payload}
sessionId={sessionId}
onToggleFullscreen={toggleFullscreen}
/>
)
: (
<iframe
key={frameKey}
ref={iframeRef}
title="mapa"
className="map-frame"
srcDoc={html}
sandbox="allow-scripts allow-same-origin allow-popups"
allow="fullscreen"
allowFullScreen
onLoad={() => {
compartilharContextoComIframe()
scheduleRecenter()
}}
/>
)
return (
<div ref={shellRef} className={`map-frame-shell${isFullscreen ? ' is-fullscreen' : ''}`}>
{mapContent}
</div>
)
})
export default MapFrame