import { useEffect, useRef, useState } from 'react'; import type { Map as LeafletMap, Marker } from 'leaflet'; import type { GeoJsonObject } from 'geojson'; import 'leaflet/dist/leaflet.css'; export type InvestmentMapFeature = { geometry: { coordinates: [number, number] }; properties: { id: number; title: string; location: string; url: string; category?: string; clusters?: string[]; corridors?: string[]; projects?: string[]; source_reference?: string | null; source_date?: string | null; }; }; export type InvestmentMapArea = { selected_territory?: boolean; location_type?: string; purpose?: string; geometry: GeoJsonObject; title: string; source_reference?: string | null; source_date?: string | null; url: string; }; const noAreas: InvestmentMapArea[] = []; export default function InteractiveInvestmentMap({ points, areas = noAreas, contextOnly = false, }: { points: InvestmentMapFeature[] | null; areas?: InvestmentMapArea[]; contextOnly?: boolean; }) { const element = useRef(null); const map = useRef(null); const markers = useRef(new Map()); const [selected, setSelected] = useState(null); const [error, setError] = useState(''); useEffect(() => { let cancelled = false; let observer: ResizeObserver | undefined; void import('leaflet') .then((L) => { if (cancelled || !element.current) return; const instance = L.map(element.current, { scrollWheelZoom: false, keyboard: true, zoomAnimation: false, fadeAnimation: false, markerZoomAnimation: false, }).setView([-18.5, 35], 5); map.current = instance; L.tileLayer( import.meta.env.VITE_MAP_TILE_URL || 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap contributors', }, ) .on('tileerror', () => { if (!cancelled) setError( 'Mapa base indisponível. Os marcadores e a lista continuam acessíveis.', ); }) .addTo(instance); L.control.scale({ imperial: false }).addTo(instance); const bounds: [number, number][] = []; const hasSelectedBoundary = areas.some((area) => area.selected_territory); const administrativeLevel: Record = { province: 1, district: 2, admin_post: 3, locality: 4, }; const closestContextLevel = contextOnly ? Math.max(0, ...areas.map((area) => administrativeLevel[area.location_type ?? ''] ?? 0)) : 0; for (const area of areas) { const label = document.createElement('p'); label.textContent = `${area.title} · ${area.purpose === 'administrative_boundary' ? 'Limite administrativo da edição indicada; não representa um local de investimento' : 'Polígono cadastrado, limites oficiais por verificar'} · Fonte: ${area.source_reference ?? 'Não indicada'} · Data: ${area.source_date ?? 'Não documentada'}`; const layer = L.geoJSON( area.geometry, area.purpose === 'administrative_boundary' ? { style: { color: '#2563eb', dashArray: '5 5', fillOpacity: 0.06, }, } : {}, ) .bindPopup(label) .addTo(instance); const extent = layer.getBounds(); const focusBoundary = hasSelectedBoundary ? area.selected_territory : !closestContextLevel || administrativeLevel[area.location_type ?? ''] === closestContextLevel; if (extent.isValid() && focusBoundary) bounds.push( [extent.getSouth(), extent.getWest()], [extent.getNorth(), extent.getEast()], ); } for (const [index, point] of (points ?? []).entries()) { const [longitude, latitude] = point.geometry.coordinates; if ( !Number.isFinite(latitude) || !Number.isFinite(longitude) || Math.abs(latitude) > 90 || Math.abs(longitude) > 180 ) continue; const { properties: p } = point; const popup = document.createElement('div'); const heading = document.createElement('strong'); heading.textContent = p.title; popup.append(heading); for (const text of [ p.category, p.location, `Fonte: ${p.source_reference ?? 'Não indicada'} · Data: ${p.source_date ?? 'Não documentada'}`, `Corredor: ${p.corridors?.join(', ') || 'Não associado'}`, `Cluster: ${p.clusters?.join(', ') || 'Não associado'}`, `Projecto âncora: ${p.projects?.join(', ') || 'Não associado'}`, ]) { const line = document.createElement('p'); line.textContent = text ?? ''; popup.append(line); } if (p.url.startsWith('/') && !p.url.startsWith('//')) { const link = document.createElement('a'); link.href = p.url; link.textContent = 'Abrir ficha detalhada'; popup.append(link); } const icon = L.divIcon({ className: '', html: `${index + 1}`, iconSize: [32, 32], iconAnchor: [16, 16], }); const marker = L.marker([latitude, longitude], { icon, title: p.title, alt: p.title, keyboard: true, }) .bindPopup(popup, { autoPan: false }) .on('click', () => setSelected(index)) .addTo(instance); markers.current.set(index, marker); bounds.push([latitude, longitude]); } if (bounds.length) instance.fitBounds(bounds, { padding: [40, 40], maxZoom: 14, animate: false, }); observer = new ResizeObserver(() => { if (!cancelled) instance.invalidateSize({ animate: false, pan: false }); }); observer.observe(element.current); }) .catch(() => { if (!cancelled) setError('Não foi possível iniciar o mapa.'); }); return () => { cancelled = true; observer?.disconnect(); map.current?.remove(); map.current = null; markers.current.clear(); }; }, [points, areas]); return (

{contextOnly ? 'Contexto administrativo' : 'Mapa dos investimentos'}

Arraste para navegar; use +/− para ampliar. Pode seleccionar um {contextOnly ? 'limite cadastrado para consultar a sua edição.' : 'investimento na lista para o localizar.'}

{error &&

{error}

}
{points === null ? (

A carregar localizações…

) : (

{contextOnly ? `${areas.length} limites administrativos disponíveis.` : `${points.length} locais com coordenadas disponíveis.`}

)}
    {(points ?? []).map((p, index) => (
  • {p.properties.category} · {p.properties.location}

    Fonte:{' '} {p.properties.source_reference ?? 'Não indicada'} · Data:{' '} {p.properties.source_date ?? 'Não documentada'}

    Abrir ficha
  • ))}
{areas.map((area, index) => (

{area.title} {' '} ·{' '} {area.purpose === 'administrative_boundary' ? 'Limite administrativo da edição indicada; não representa um local de investimento' : 'Polígono cadastrado; comparação com limite administrativo pendente'}{' '} · Fonte: {area.source_reference ?? 'Não indicada'} · Data:{' '} {area.source_date ?? 'Não documentada'}

))}
); }