Files
dh-inspeccion-v2/web-v2/src/features/map/DhMap.tsx
T
admin cab9cd7c01
DH V2 CI / API · typecheck, tests, build (push) Successful in 38s
Production dependency audit / API · production dependencies (push) Successful in 9s
Production dependency audit / WEB · production dependencies (push) Successful in 8s
DH V2 CI / WEB · typecheck, build (push) Successful in 1m36s
DH V2 CI / Docker / migrations / production images (push) Successful in 1m45s
DH V2 CI / Promote verified main to deploy (push) Successful in 3s
Android CI / RC / Android · lint, tests, debug APK, release compile (push) Successful in 4m5s
fix: restore map geometries and SMTP configuration
2026-09-15 23:58:49 -03:00

167 lines
5.9 KiB
TypeScript

import { useEffect, useRef } from 'react';
import {
Map,
NavigationControl,
type GeoJSONSource,
type MapGeoJSONFeature,
} from 'maplibre-gl';
import type { MapAssetFeatureCollection } from '../../lib/api';
const osmStyle = {
version: 8 as const,
sources: {
osm: {
type: 'raster' as const,
tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
tileSize: 256,
attribution: '© OpenStreetMap contributors',
},
},
layers: [{ id: 'osm', type: 'raster' as const, source: 'osm' }],
};
const interactiveLayers = ['assets-points', 'assets-lines', 'assets-polygons'];
function boundsFromFeatures(collection: MapAssetFeatureCollection) {
const positions: Array<[number, number]> = [];
collection.features.forEach((feature) => {
if (feature.geometry.type === 'Point') positions.push(feature.geometry.coordinates);
if (feature.geometry.type === 'LineString') positions.push(...feature.geometry.coordinates);
if (feature.geometry.type === 'Polygon') feature.geometry.coordinates.forEach((ring) => positions.push(...ring));
});
if (!positions.length) return null;
return positions.reduce<[number, number, number, number]>((result, point) => [
Math.min(result[0], point[0]),
Math.min(result[1], point[1]),
Math.max(result[2], point[0]),
Math.max(result[3], point[1]),
], [positions[0]![0], positions[0]![1], positions[0]![0], positions[0]![1]]);
}
export function DhMap({
data,
selectedId,
onSelect,
}: {
data: MapAssetFeatureCollection;
selectedId: string | null;
onSelect: (id: string | null) => void;
}) {
const containerRef = useRef<HTMLDivElement | null>(null);
const mapRef = useRef<Map | null>(null);
const onSelectRef = useRef(onSelect);
const dataRef = useRef(data);
onSelectRef.current = onSelect;
dataRef.current = data;
useEffect(() => {
if (!containerRef.current) return;
const map = new Map({
container: containerRef.current,
style: osmStyle,
center: [-68.8458, -32.8895],
zoom: 6,
});
mapRef.current = map;
map.addControl(new NavigationControl(), 'top-right');
map.on('load', () => {
map.addSource('assets', { type: 'geojson', data: dataRef.current as never });
map.addLayer({
id: 'assets-polygons', type: 'fill', source: 'assets',
filter: ['==', ['geometry-type'], 'Polygon'],
paint: { 'fill-color': '#2864dc', 'fill-opacity': 0.22, 'fill-outline-color': '#184caf' },
});
map.addLayer({
id: 'assets-lines', type: 'line', source: 'assets',
filter: ['==', ['geometry-type'], 'LineString'],
paint: { 'line-color': '#2864dc', 'line-width': 3 },
});
map.addLayer({
id: 'assets-points', type: 'circle', source: 'assets',
filter: ['==', ['geometry-type'], 'Point'],
paint: {
'circle-radius': 7, 'circle-color': '#2864dc',
'circle-stroke-color': '#ffffff', 'circle-stroke-width': 2,
},
});
map.addLayer({
id: 'assets-selected-polygons', type: 'line', source: 'assets',
filter: ['all', ['==', ['geometry-type'], 'Polygon'], ['==', ['get', 'id'], '']],
paint: { 'line-color': '#f59e0b', 'line-width': 4 },
});
map.addLayer({
id: 'assets-selected-lines', type: 'line', source: 'assets',
filter: ['all', ['==', ['geometry-type'], 'LineString'], ['==', ['get', 'id'], '']],
paint: { 'line-color': '#f59e0b', 'line-width': 6 },
});
map.addLayer({
id: 'assets-selected-points', type: 'circle', source: 'assets',
filter: ['all', ['==', ['geometry-type'], 'Point'], ['==', ['get', 'id'], '']],
paint: {
'circle-radius': 10, 'circle-color': '#f59e0b',
'circle-stroke-color': '#ffffff', 'circle-stroke-width': 3,
},
});
const click = (event: { features?: MapGeoJSONFeature[] }) => {
const id = event.features?.[0]?.properties?.id;
onSelectRef.current(typeof id === 'string' ? id : null);
};
interactiveLayers.forEach((layer) => {
map.on('click', layer, click);
map.on('mouseenter', layer, () => { map.getCanvas().style.cursor = 'pointer'; });
map.on('mouseleave', layer, () => { map.getCanvas().style.cursor = ''; });
});
map.on('click', (event) => {
const hits = map.queryRenderedFeatures(event.point, { layers: interactiveLayers });
if (!hits.length) onSelectRef.current(null);
});
const bounds = boundsFromFeatures(dataRef.current);
if (bounds) {
if (bounds[0] === bounds[2] && bounds[1] === bounds[3]) {
map.flyTo({ center: [bounds[0], bounds[1]], zoom: 13 });
} else {
map.fitBounds([[bounds[0], bounds[1]], [bounds[2], bounds[3]]], { padding: 55, maxZoom: 15 });
}
}
});
return () => {
mapRef.current = null;
map.remove();
};
}, []);
useEffect(() => {
const map = mapRef.current;
if (!map?.isStyleLoaded()) return;
(map.getSource('assets') as GeoJSONSource | undefined)?.setData(data as never);
const bounds = boundsFromFeatures(data);
if (bounds) {
if (bounds[0] === bounds[2] && bounds[1] === bounds[3]) {
map.flyTo({ center: [bounds[0], bounds[1]], zoom: 13 });
} else {
map.fitBounds([[bounds[0], bounds[1]], [bounds[2], bounds[3]]], { padding: 55, maxZoom: 15 });
}
}
}, [data]);
useEffect(() => {
const map = mapRef.current;
if (!map?.isStyleLoaded()) return;
const id = selectedId ?? '__none__';
const filters: Array<[string, string]> = [
['assets-selected-points', 'Point'],
['assets-selected-lines', 'LineString'],
['assets-selected-polygons', 'Polygon'],
];
filters.forEach(([layer, geometryType]) => {
map.setFilter(layer, ['all', ['==', ['geometry-type'], geometryType], ['==', ['get', 'id'], id]]);
});
}, [selectedId]);
return <div ref={containerRef} className="map-canvas" />;
}