demarches-normaliennes/app/javascript/components/MapReader/components/GeoJSONLayer.tsx

266 lines
6.4 KiB
TypeScript

import React, { useCallback, useEffect, useMemo } from 'react';
import { Popup, LngLatBoundsLike } from 'maplibre-gl';
import type { Feature, FeatureCollection } from 'geojson';
import { useMapLibre } from '../../shared/maplibre/MapLibre';
import {
useFitBounds,
useEvent,
EventHandler,
useMapEvent,
useFlyTo
} from '../../shared/maplibre/hooks';
import {
filterFeatureCollection,
findFeature,
getBounds,
getCenter,
filterFeatureCollectionByGeometryType
} from '../../shared/maplibre/utils';
export function GeoJSONLayer({
featureCollection
}: {
featureCollection: FeatureCollection;
}) {
const map = useMapLibre();
const popup = useMemo(
() =>
new Popup({
closeButton: false,
closeOnClick: false
}),
[]
);
const onMouseEnter = useCallback<EventHandler>(
(event) => {
const feature = event.features && event.features[0];
if (feature?.properties && feature.properties.description) {
const coordinates = getCenter(feature.geometry, event.lngLat);
const description = feature.properties.description;
map.getCanvas().style.cursor = 'pointer';
popup.setLngLat(coordinates).setHTML(description).addTo(map);
} else {
popup.remove();
}
},
[map, popup]
);
const onMouseLeave = useCallback(() => {
map.getCanvas().style.cursor = '';
popup.remove();
}, [map, popup]);
useExternalEvents(featureCollection);
const polygons = filterFeatureCollectionByGeometryType(
filterFeatureCollection(featureCollection, 'selection_utilisateur'),
'Polygon'
);
const lines = filterFeatureCollectionByGeometryType(
filterFeatureCollection(featureCollection, 'selection_utilisateur'),
'LineString'
);
const points = filterFeatureCollectionByGeometryType(
filterFeatureCollection(featureCollection, 'selection_utilisateur'),
'Point'
);
return (
<>
{polygons.features.map((feature) => (
<PolygonLayer
key={feature.properties?.id}
feature={feature}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
/>
))}
{lines.features.map((feature) => (
<LineStringLayer
key={feature.properties?.id}
feature={feature}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
/>
))}
{points.features.map((feature) => (
<PointLayer
key={feature.properties?.id}
feature={feature}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
/>
))}
</>
);
}
function useExternalEvents(featureCollection: FeatureCollection) {
const fitBounds = useFitBounds();
const flyTo = useFlyTo();
const onFeatureFocus = useCallback(
({ detail }) => {
const { id } = detail;
const feature = findFeature(featureCollection, id);
if (feature) {
fitBounds(getBounds(feature.geometry));
}
},
[featureCollection, fitBounds]
);
const onZoomFocus = useCallback(
({ detail }) => {
const { feature } = detail;
if (feature) {
flyTo(17, feature.geometry.coordinates);
}
},
[flyTo]
);
useEffect(() => {
fitBounds(featureCollection.bbox as LngLatBoundsLike);
// We only want to zoom on bbox on component mount.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fitBounds]);
useEvent('map:feature:focus', onFeatureFocus);
useEvent('map:zoom', onZoomFocus);
}
function LineStringLayer({
feature,
onMouseEnter,
onMouseLeave
}: {
feature: Feature;
onMouseEnter: EventHandler;
onMouseLeave: EventHandler;
}) {
const map = useMapLibre();
const sourceId = String(feature.properties?.id);
const layerId = `${sourceId}-layer`;
const render = () => {
if (!map.getSource(sourceId)) {
map
.addSource(sourceId, {
type: 'geojson',
data: feature
})
.addLayer({
id: layerId,
source: sourceId,
type: 'line',
paint: lineStringSelectionLine
});
}
};
useEffect(render, [map, layerId, sourceId, feature]);
useMapEvent('styledata', render);
useMapEvent('mouseenter', onMouseEnter, layerId);
useMapEvent('mouseleave', onMouseLeave, layerId);
return null;
}
function PointLayer({
feature,
onMouseEnter,
onMouseLeave
}: {
feature: Feature;
onMouseEnter: EventHandler;
onMouseLeave: EventHandler;
}) {
const map = useMapLibre();
const sourceId = String(feature.properties?.id);
const layerId = `${sourceId}-layer`;
const render = () => {
if (!map.getSource(sourceId)) {
map
.addSource(sourceId, {
type: 'geojson',
data: feature
})
.addLayer({
id: layerId,
source: sourceId,
type: 'circle',
paint: pointSelectionCircle
});
}
};
useEffect(render, [map, layerId, sourceId, feature]);
useMapEvent('styledata', render);
useMapEvent('mouseenter', onMouseEnter, layerId);
useMapEvent('mouseleave', onMouseLeave, layerId);
return null;
}
function PolygonLayer({
feature,
onMouseEnter,
onMouseLeave
}: {
feature: Feature;
onMouseEnter: EventHandler;
onMouseLeave: EventHandler;
}) {
const map = useMapLibre();
const sourceId = String(feature.properties?.id);
const layerId = `${sourceId}-layer`;
const lineLayerId = `${sourceId}-line-layer`;
const render = () => {
if (!map.getSource(sourceId)) {
map
.addSource(sourceId, {
type: 'geojson',
data: feature
})
.addLayer({
id: lineLayerId,
source: sourceId,
type: 'line',
paint: polygonSelectionLine
})
.addLayer({
id: layerId,
source: sourceId,
type: 'fill',
paint: polygonSelectionFill
});
}
};
useEffect(render, [map, layerId, lineLayerId, sourceId, feature]);
useMapEvent('styledata', render);
useMapEvent('mouseenter', onMouseEnter, layerId);
useMapEvent('mouseleave', onMouseLeave, layerId);
return null;
}
const polygonSelectionFill = {
'fill-color': '#EC3323',
'fill-opacity': 0.5
};
const polygonSelectionLine = {
'line-color': 'rgba(255, 0, 0, 1)',
'line-width': 4
};
const lineStringSelectionLine = {
'line-color': 'rgba(55, 42, 127, 1.00)',
'line-width': 3
};
const pointSelectionCircle = {
'circle-color': '#EC3323'
};