746 lines
22 KiB
TypeScript
746 lines
22 KiB
TypeScript
import { useEffect, useRef, useState } from 'react'
|
|
import maplibregl from 'maplibre-gl'
|
|
import 'maplibre-gl/dist/maplibre-gl.css'
|
|
import { PRIMARY_FOCUS_CENTER } from '../config/primaryFocus'
|
|
import { featureCollectionBounds } from '../lib/geojsonBounds'
|
|
import type { MapImageOverlay, MapViewportState, VectorSelectionBBox } from '../types'
|
|
|
|
interface GeoMapProps {
|
|
data: GeoJSON.FeatureCollection | null
|
|
dataFillColor?: string
|
|
dataLineColor?: string
|
|
areaData?: GeoJSON.FeatureCollection | null
|
|
selectedFeature?: GeoJSON.Feature | null
|
|
selectionData?: GeoJSON.FeatureCollection | null
|
|
qaEvidenceData?: GeoJSON.FeatureCollection | null
|
|
imageOverlays?: MapImageOverlay[]
|
|
selectionBbox?: { min_x: number; min_y: number; max_x: number; max_y: number } | null
|
|
bboxSelectionMode?: boolean
|
|
visible?: boolean
|
|
opacity?: number
|
|
areaVisible?: boolean
|
|
areaOpacity?: number
|
|
fitDataOnChange?: boolean
|
|
onFeatureSelect?: (feature: GeoJSON.Feature | null) => void
|
|
onMapCoordinateSelect?: (coordinate: [number, number]) => void
|
|
onMapBboxPreview?: (bbox: VectorSelectionBBox) => void
|
|
onMapBboxSelect?: (bbox: VectorSelectionBBox) => void
|
|
onViewportChange?: (viewport: MapViewportState) => void
|
|
}
|
|
|
|
const EMPTY_FEATURE_COLLECTION: GeoJSON.FeatureCollection = {
|
|
type: 'FeatureCollection',
|
|
features: [],
|
|
}
|
|
|
|
const DEFAULT_ROAD_BASEMAP_STYLE: maplibregl.StyleSpecification = {
|
|
version: 8,
|
|
sources: {
|
|
'osm-standard': {
|
|
type: 'raster',
|
|
tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
|
|
tileSize: 256,
|
|
attribution: '© OpenStreetMap contributors',
|
|
maxzoom: 19,
|
|
},
|
|
},
|
|
layers: [
|
|
{
|
|
id: 'osm-standard',
|
|
type: 'raster',
|
|
source: 'osm-standard',
|
|
},
|
|
],
|
|
}
|
|
|
|
function datasetFillColor(fallbackColor: string): maplibregl.ExpressionSpecification {
|
|
return [
|
|
'case',
|
|
['==', ['get', 'layer_type'], 'municipality_boundary'],
|
|
'#0f766e',
|
|
[
|
|
'match',
|
|
['get', 'change_type'],
|
|
'added',
|
|
'#16a34a',
|
|
'removed',
|
|
'#dc2626',
|
|
'modified',
|
|
'#d97706',
|
|
'unchanged',
|
|
'#2563eb',
|
|
fallbackColor,
|
|
],
|
|
]
|
|
}
|
|
|
|
function datasetLineColor(fallbackColor: string): maplibregl.ExpressionSpecification {
|
|
return [
|
|
'case',
|
|
['==', ['get', 'layer_type'], 'municipality_boundary'],
|
|
'#0f5f59',
|
|
[
|
|
'match',
|
|
['get', 'change_type'],
|
|
'added',
|
|
'#15803d',
|
|
'removed',
|
|
'#b91c1c',
|
|
'modified',
|
|
'#b45309',
|
|
'unchanged',
|
|
'#1d4ed8',
|
|
fallbackColor,
|
|
],
|
|
]
|
|
}
|
|
|
|
function defaultMapStyle(): string | maplibregl.StyleSpecification {
|
|
return import.meta.env.VITE_MAP_STYLE_URL || DEFAULT_ROAD_BASEMAP_STYLE
|
|
}
|
|
|
|
function collectCoordinates(featureCollection: GeoJSON.FeatureCollection): maplibregl.LngLatBoundsLike | null {
|
|
const bounds = featureCollectionBounds(featureCollection)
|
|
if (!bounds) {
|
|
return null
|
|
}
|
|
return [
|
|
[bounds.minX, bounds.minY],
|
|
[bounds.maxX, bounds.maxY],
|
|
]
|
|
}
|
|
|
|
function bboxToFeatureCollection(
|
|
bbox: { min_x: number; min_y: number; max_x: number; max_y: number } | null | undefined,
|
|
): GeoJSON.FeatureCollection {
|
|
if (!bbox) {
|
|
return EMPTY_FEATURE_COLLECTION
|
|
}
|
|
return {
|
|
type: 'FeatureCollection',
|
|
features: [
|
|
{
|
|
type: 'Feature',
|
|
geometry: {
|
|
type: 'Polygon',
|
|
coordinates: [
|
|
[
|
|
[bbox.min_x, bbox.min_y],
|
|
[bbox.max_x, bbox.min_y],
|
|
[bbox.max_x, bbox.max_y],
|
|
[bbox.min_x, bbox.max_y],
|
|
[bbox.min_x, bbox.min_y],
|
|
],
|
|
],
|
|
},
|
|
properties: {
|
|
layer_type: 'selection_bbox',
|
|
},
|
|
},
|
|
],
|
|
}
|
|
}
|
|
|
|
function GeoMap({
|
|
data,
|
|
dataFillColor = '#f97316',
|
|
dataLineColor = '#ea580c',
|
|
areaData = null,
|
|
selectedFeature = null,
|
|
selectionData = null,
|
|
qaEvidenceData = null,
|
|
imageOverlays = [],
|
|
selectionBbox = null,
|
|
bboxSelectionMode = false,
|
|
visible = true,
|
|
opacity = 0.4,
|
|
areaVisible = true,
|
|
areaOpacity = 0.18,
|
|
fitDataOnChange = true,
|
|
onFeatureSelect,
|
|
onMapCoordinateSelect,
|
|
onMapBboxPreview,
|
|
onMapBboxSelect,
|
|
onViewportChange,
|
|
}: GeoMapProps): JSX.Element {
|
|
const containerRef = useRef<HTMLDivElement | null>(null)
|
|
const mapRef = useRef<maplibregl.Map | null>(null)
|
|
const onFeatureSelectRef = useRef<GeoMapProps['onFeatureSelect']>(onFeatureSelect)
|
|
const onMapCoordinateSelectRef = useRef<GeoMapProps['onMapCoordinateSelect']>(onMapCoordinateSelect)
|
|
const onMapBboxPreviewRef = useRef<GeoMapProps['onMapBboxPreview']>(onMapBboxPreview)
|
|
const onMapBboxSelectRef = useRef<GeoMapProps['onMapBboxSelect']>(onMapBboxSelect)
|
|
const onViewportChangeRef = useRef<GeoMapProps['onViewportChange']>(onViewportChange)
|
|
const bboxSelectionModeRef = useRef(bboxSelectionMode)
|
|
const areaDataRef = useRef<GeoJSON.FeatureCollection | null>(areaData)
|
|
const dataRef = useRef<GeoJSON.FeatureCollection | null>(data)
|
|
const fitDataOnChangeRef = useRef(fitDataOnChange)
|
|
const imageOverlayIdsRef = useRef<string[]>([])
|
|
const [mapStyleReady, setMapStyleReady] = useState(false)
|
|
|
|
areaDataRef.current = areaData
|
|
dataRef.current = data
|
|
fitDataOnChangeRef.current = fitDataOnChange
|
|
|
|
useEffect(() => {
|
|
onFeatureSelectRef.current = onFeatureSelect
|
|
}, [onFeatureSelect])
|
|
|
|
useEffect(() => {
|
|
onMapCoordinateSelectRef.current = onMapCoordinateSelect
|
|
}, [onMapCoordinateSelect])
|
|
|
|
useEffect(() => {
|
|
onMapBboxPreviewRef.current = onMapBboxPreview
|
|
}, [onMapBboxPreview])
|
|
|
|
useEffect(() => {
|
|
onMapBboxSelectRef.current = onMapBboxSelect
|
|
}, [onMapBboxSelect])
|
|
|
|
useEffect(() => {
|
|
onViewportChangeRef.current = onViewportChange
|
|
}, [onViewportChange])
|
|
|
|
useEffect(() => {
|
|
bboxSelectionModeRef.current = bboxSelectionMode
|
|
if (mapRef.current) {
|
|
mapRef.current.getCanvas().style.cursor = bboxSelectionMode ? 'crosshair' : ''
|
|
if (bboxSelectionMode) {
|
|
mapRef.current.dragPan.disable()
|
|
} else {
|
|
mapRef.current.dragPan.enable()
|
|
}
|
|
}
|
|
}, [bboxSelectionMode])
|
|
|
|
useEffect(() => {
|
|
if (!containerRef.current || mapRef.current) {
|
|
return
|
|
}
|
|
|
|
const map = new maplibregl.Map({
|
|
container: containerRef.current,
|
|
style: defaultMapStyle(),
|
|
center: PRIMARY_FOCUS_CENTER,
|
|
zoom: 11,
|
|
attributionControl: false,
|
|
})
|
|
const resizeObserver = new ResizeObserver(() => {
|
|
map.resize()
|
|
const fitCollection = areaDataRef.current
|
|
?? (fitDataOnChangeRef.current ? dataRef.current : null)
|
|
const bounds = fitCollection ? collectCoordinates(fitCollection) : null
|
|
if (bounds && map.isStyleLoaded()) {
|
|
map.fitBounds(bounds, { padding: 40, duration: 0 })
|
|
}
|
|
})
|
|
resizeObserver.observe(containerRef.current)
|
|
map.addControl(new maplibregl.NavigationControl(), 'top-right')
|
|
map.addControl(
|
|
new maplibregl.AttributionControl({
|
|
compact: true,
|
|
}),
|
|
'bottom-right',
|
|
)
|
|
const emitViewport = () => {
|
|
const bounds = map.getBounds()
|
|
onViewportChangeRef.current?.({
|
|
bbox: {
|
|
min_x: bounds.getWest(),
|
|
min_y: bounds.getSouth(),
|
|
max_x: bounds.getEast(),
|
|
max_y: bounds.getNorth(),
|
|
crs: 'EPSG:4326',
|
|
},
|
|
zoom: map.getZoom(),
|
|
})
|
|
}
|
|
map.on('load', () => {
|
|
map.resize()
|
|
setMapStyleReady(true)
|
|
emitViewport()
|
|
})
|
|
map.on('moveend', emitViewport)
|
|
let dragStart: [number, number] | null = null
|
|
let draggedSelection = false
|
|
const bboxFromCoordinates = (start: [number, number], end: [number, number]): VectorSelectionBBox => ({
|
|
min_x: Math.min(start[0], end[0]),
|
|
min_y: Math.min(start[1], end[1]),
|
|
max_x: Math.max(start[0], end[0]),
|
|
max_y: Math.max(start[1], end[1]),
|
|
crs: 'EPSG:4326',
|
|
})
|
|
map.on('mousedown', (event) => {
|
|
if (!bboxSelectionModeRef.current || event.originalEvent.button !== 0) {
|
|
return
|
|
}
|
|
event.preventDefault()
|
|
dragStart = [event.lngLat.lng, event.lngLat.lat]
|
|
draggedSelection = false
|
|
map.getCanvas().style.cursor = 'crosshair'
|
|
})
|
|
map.on('mousemove', (event) => {
|
|
if (!bboxSelectionModeRef.current || !dragStart) {
|
|
return
|
|
}
|
|
draggedSelection = true
|
|
onMapBboxPreviewRef.current?.(bboxFromCoordinates(dragStart, [event.lngLat.lng, event.lngLat.lat]))
|
|
})
|
|
map.on('mouseup', (event) => {
|
|
if (!bboxSelectionModeRef.current || !dragStart) {
|
|
return
|
|
}
|
|
const start = dragStart
|
|
dragStart = null
|
|
const bbox = bboxFromCoordinates(start, [event.lngLat.lng, event.lngLat.lat])
|
|
if (bbox.max_x > bbox.min_x && bbox.max_y > bbox.min_y) {
|
|
onMapBboxSelectRef.current?.(bbox)
|
|
}
|
|
})
|
|
map.on('click', (event) => {
|
|
if (bboxSelectionModeRef.current) {
|
|
if (draggedSelection) {
|
|
draggedSelection = false
|
|
return
|
|
}
|
|
onMapCoordinateSelectRef.current?.([event.lngLat.lng, event.lngLat.lat])
|
|
return
|
|
}
|
|
const layers = [
|
|
'qa-evidence-fill',
|
|
'qa-evidence-line',
|
|
'qa-evidence-circle',
|
|
'selection-result-fill',
|
|
'selection-result-line',
|
|
'selection-result-circle',
|
|
'dataset-fill',
|
|
'dataset-line',
|
|
'area-fill',
|
|
'area-line',
|
|
].filter((layerId) => map.getLayer(layerId))
|
|
if (layers.length === 0) {
|
|
onFeatureSelectRef.current?.(null)
|
|
return
|
|
}
|
|
const features = map.queryRenderedFeatures(event.point, {
|
|
layers,
|
|
})
|
|
if (features.length === 0) {
|
|
onFeatureSelectRef.current?.(null)
|
|
return
|
|
}
|
|
const feature = features[0] as unknown as GeoJSON.Feature
|
|
onFeatureSelectRef.current?.(feature)
|
|
})
|
|
mapRef.current = map
|
|
|
|
return () => {
|
|
resizeObserver.disconnect()
|
|
map.remove()
|
|
mapRef.current = null
|
|
setMapStyleReady(false)
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
const map = mapRef.current
|
|
if (!map || !mapStyleReady || !map.isStyleLoaded()) {
|
|
return
|
|
}
|
|
for (const overlayId of [...imageOverlayIdsRef.current].reverse()) {
|
|
if (map.getLayer(overlayId)) {
|
|
map.removeLayer(overlayId)
|
|
}
|
|
if (map.getSource(overlayId)) {
|
|
map.removeSource(overlayId)
|
|
}
|
|
}
|
|
imageOverlayIdsRef.current = []
|
|
const beforeLayer = ['area-fill', 'dataset-fill', 'selection-bbox-fill'].find((layerId) => map.getLayer(layerId))
|
|
imageOverlays.forEach((imageOverlay, index) => {
|
|
const overlayId = `bounded-raster-${index}`
|
|
const [minX, minY, maxX, maxY] = imageOverlay.bbox
|
|
map.addSource(overlayId, {
|
|
type: 'image',
|
|
url: imageOverlay.url,
|
|
coordinates: [
|
|
[minX, maxY],
|
|
[maxX, maxY],
|
|
[maxX, minY],
|
|
[minX, minY],
|
|
],
|
|
})
|
|
map.addLayer({
|
|
id: overlayId,
|
|
type: 'raster',
|
|
source: overlayId,
|
|
paint: { 'raster-opacity': imageOverlay.opacity ?? 0.88 },
|
|
}, beforeLayer)
|
|
imageOverlayIdsRef.current.push(overlayId)
|
|
})
|
|
}, [imageOverlays, mapStyleReady])
|
|
|
|
useEffect(() => {
|
|
const map = mapRef.current
|
|
if (!map || !mapStyleReady || !map.isStyleLoaded()) {
|
|
return
|
|
}
|
|
|
|
if (map.getSource('dataset')) {
|
|
if (data) {
|
|
;(map.getSource('dataset') as maplibregl.GeoJSONSource).setData(data)
|
|
} else {
|
|
if (map.getLayer('dataset-fill')) {
|
|
map.removeLayer('dataset-fill')
|
|
}
|
|
if (map.getLayer('dataset-line')) {
|
|
map.removeLayer('dataset-line')
|
|
}
|
|
map.removeSource('dataset')
|
|
return
|
|
}
|
|
} else if (data) {
|
|
map.addSource('dataset', { type: 'geojson', data })
|
|
map.addLayer({
|
|
id: 'dataset-fill',
|
|
type: 'fill',
|
|
source: 'dataset',
|
|
paint: {
|
|
'fill-color': datasetFillColor(dataFillColor),
|
|
'fill-opacity': 0.4,
|
|
},
|
|
})
|
|
map.addLayer({
|
|
id: 'dataset-line',
|
|
type: 'line',
|
|
source: 'dataset',
|
|
paint: {
|
|
'line-color': datasetLineColor(dataLineColor),
|
|
'line-width': ['interpolate', ['linear'], ['zoom'], 8, 0.25, 12, 0.8, 16, 2],
|
|
},
|
|
})
|
|
}
|
|
|
|
if (data && fitDataOnChange && !areaData) {
|
|
const collection = data
|
|
if (collection.type === 'FeatureCollection' && collection.features.length > 0) {
|
|
const bounds = collectCoordinates(collection)
|
|
if (bounds) {
|
|
map.fitBounds(bounds, { padding: 40 })
|
|
}
|
|
}
|
|
}
|
|
}, [areaData, data, dataFillColor, dataLineColor, fitDataOnChange, mapStyleReady])
|
|
|
|
useEffect(() => {
|
|
const map = mapRef.current
|
|
if (!map || !mapStyleReady || !map.isStyleLoaded()) {
|
|
return
|
|
}
|
|
if (map.getLayer('dataset-fill')) {
|
|
map.setPaintProperty('dataset-fill', 'fill-color', datasetFillColor(dataFillColor))
|
|
}
|
|
if (map.getLayer('dataset-line')) {
|
|
map.setPaintProperty('dataset-line', 'line-color', datasetLineColor(dataLineColor))
|
|
}
|
|
}, [dataFillColor, dataLineColor, mapStyleReady])
|
|
|
|
useEffect(() => {
|
|
const map = mapRef.current
|
|
if (!map || !mapStyleReady || !map.isStyleLoaded()) {
|
|
return
|
|
}
|
|
|
|
if (map.getSource('area')) {
|
|
if (areaData) {
|
|
;(map.getSource('area') as maplibregl.GeoJSONSource).setData(areaData)
|
|
} else {
|
|
if (map.getLayer('area-fill')) {
|
|
map.removeLayer('area-fill')
|
|
}
|
|
if (map.getLayer('area-line')) {
|
|
map.removeLayer('area-line')
|
|
}
|
|
map.removeSource('area')
|
|
return
|
|
}
|
|
} else if (areaData) {
|
|
map.addSource('area', { type: 'geojson', data: areaData })
|
|
map.addLayer(
|
|
{
|
|
id: 'area-fill',
|
|
type: 'fill',
|
|
source: 'area',
|
|
paint: {
|
|
'fill-color': '#0f766e',
|
|
'fill-opacity': 0.18,
|
|
},
|
|
},
|
|
map.getLayer('dataset-fill') ? 'dataset-fill' : undefined,
|
|
)
|
|
map.addLayer(
|
|
{
|
|
id: 'area-line',
|
|
type: 'line',
|
|
source: 'area',
|
|
paint: {
|
|
'line-color': '#0f766e',
|
|
'line-width': 3,
|
|
'line-dasharray': [2, 1],
|
|
},
|
|
},
|
|
map.getLayer('dataset-line') ? 'dataset-line' : undefined,
|
|
)
|
|
}
|
|
|
|
const activeCollection = areaData ?? (fitDataOnChange ? data : null)
|
|
if (activeCollection) {
|
|
const bounds = collectCoordinates(activeCollection)
|
|
if (bounds) {
|
|
map.fitBounds(bounds, { padding: 40 })
|
|
}
|
|
}
|
|
}, [areaData, data, fitDataOnChange, mapStyleReady])
|
|
|
|
useEffect(() => {
|
|
const map = mapRef.current
|
|
if (!map || !mapStyleReady || !map.isStyleLoaded()) {
|
|
return
|
|
}
|
|
const visibility = visible ? 'visible' : 'none'
|
|
if (map.getLayer('dataset-fill')) {
|
|
map.setLayoutProperty('dataset-fill', 'visibility', visibility)
|
|
map.setPaintProperty('dataset-fill', 'fill-opacity', opacity)
|
|
}
|
|
if (map.getLayer('dataset-line')) {
|
|
map.setLayoutProperty('dataset-line', 'visibility', visibility)
|
|
map.setPaintProperty('dataset-line', 'line-opacity', visible ? 1 : 0)
|
|
}
|
|
}, [visible, opacity, data, mapStyleReady])
|
|
|
|
useEffect(() => {
|
|
const map = mapRef.current
|
|
if (!map || !mapStyleReady || !map.isStyleLoaded()) {
|
|
return
|
|
}
|
|
const visibility = areaVisible ? 'visible' : 'none'
|
|
if (map.getLayer('area-fill')) {
|
|
map.setLayoutProperty('area-fill', 'visibility', visibility)
|
|
map.setPaintProperty('area-fill', 'fill-opacity', areaOpacity)
|
|
}
|
|
if (map.getLayer('area-line')) {
|
|
map.setLayoutProperty('area-line', 'visibility', visibility)
|
|
map.setPaintProperty('area-line', 'line-opacity', areaVisible ? 1 : 0)
|
|
}
|
|
}, [areaVisible, areaOpacity, areaData, mapStyleReady])
|
|
|
|
useEffect(() => {
|
|
const map = mapRef.current
|
|
if (!map || !mapStyleReady || !map.isStyleLoaded()) {
|
|
return
|
|
}
|
|
|
|
const selectedCollection: GeoJSON.FeatureCollection = selectedFeature
|
|
? { type: 'FeatureCollection', features: [selectedFeature] }
|
|
: EMPTY_FEATURE_COLLECTION
|
|
|
|
if (map.getSource('selected-feature')) {
|
|
;(map.getSource('selected-feature') as maplibregl.GeoJSONSource).setData(selectedCollection)
|
|
return
|
|
}
|
|
|
|
map.addSource('selected-feature', { type: 'geojson', data: selectedCollection })
|
|
map.addLayer({
|
|
id: 'selected-feature-fill',
|
|
type: 'fill',
|
|
source: 'selected-feature',
|
|
filter: ['match', ['geometry-type'], ['Polygon', 'MultiPolygon'], true, false],
|
|
paint: {
|
|
'fill-color': '#fde047',
|
|
'fill-opacity': 0.32,
|
|
},
|
|
})
|
|
map.addLayer({
|
|
id: 'selected-feature-line',
|
|
type: 'line',
|
|
source: 'selected-feature',
|
|
filter: ['match', ['geometry-type'], ['Polygon', 'MultiPolygon', 'LineString', 'MultiLineString'], true, false],
|
|
paint: {
|
|
'line-color': '#854d0e',
|
|
'line-width': 4,
|
|
},
|
|
})
|
|
map.addLayer({
|
|
id: 'selected-feature-circle',
|
|
type: 'circle',
|
|
source: 'selected-feature',
|
|
filter: ['match', ['geometry-type'], ['Point', 'MultiPoint'], true, false],
|
|
paint: {
|
|
'circle-color': '#fde047',
|
|
'circle-radius': 7,
|
|
'circle-stroke-color': '#854d0e',
|
|
'circle-stroke-width': 2,
|
|
},
|
|
})
|
|
}, [selectedFeature, mapStyleReady])
|
|
|
|
useEffect(() => {
|
|
const map = mapRef.current
|
|
if (!map || !mapStyleReady || !map.isStyleLoaded()) {
|
|
return
|
|
}
|
|
|
|
const bboxCollection = bboxToFeatureCollection(selectionBbox)
|
|
if (map.getSource('selection-bbox')) {
|
|
;(map.getSource('selection-bbox') as maplibregl.GeoJSONSource).setData(bboxCollection)
|
|
} else {
|
|
map.addSource('selection-bbox', { type: 'geojson', data: bboxCollection })
|
|
map.addLayer({
|
|
id: 'selection-bbox-fill',
|
|
type: 'fill',
|
|
source: 'selection-bbox',
|
|
paint: {
|
|
'fill-color': '#38bdf8',
|
|
'fill-opacity': 0.12,
|
|
},
|
|
})
|
|
map.addLayer({
|
|
id: 'selection-bbox-line',
|
|
type: 'line',
|
|
source: 'selection-bbox',
|
|
paint: {
|
|
'line-color': '#0369a1',
|
|
'line-width': 2,
|
|
'line-dasharray': [2, 1],
|
|
},
|
|
})
|
|
}
|
|
}, [selectionBbox, mapStyleReady])
|
|
|
|
useEffect(() => {
|
|
const map = mapRef.current
|
|
if (!map || !mapStyleReady || !map.isStyleLoaded()) {
|
|
return
|
|
}
|
|
|
|
const resultCollection = selectionData ?? EMPTY_FEATURE_COLLECTION
|
|
if (map.getSource('selection-result')) {
|
|
;(map.getSource('selection-result') as maplibregl.GeoJSONSource).setData(resultCollection)
|
|
return
|
|
}
|
|
|
|
map.addSource('selection-result', { type: 'geojson', data: resultCollection })
|
|
map.addLayer({
|
|
id: 'selection-result-fill',
|
|
type: 'fill',
|
|
source: 'selection-result',
|
|
filter: ['match', ['geometry-type'], ['Polygon', 'MultiPolygon'], true, false],
|
|
paint: {
|
|
'fill-color': '#7c3aed',
|
|
'fill-opacity': 0.24,
|
|
},
|
|
})
|
|
map.addLayer({
|
|
id: 'selection-result-line',
|
|
type: 'line',
|
|
source: 'selection-result',
|
|
filter: ['match', ['geometry-type'], ['Polygon', 'MultiPolygon', 'LineString', 'MultiLineString'], true, false],
|
|
paint: {
|
|
'line-color': '#5b21b6',
|
|
'line-width': 3,
|
|
},
|
|
})
|
|
map.addLayer({
|
|
id: 'selection-result-circle',
|
|
type: 'circle',
|
|
source: 'selection-result',
|
|
filter: ['match', ['geometry-type'], ['Point', 'MultiPoint'], true, false],
|
|
paint: {
|
|
'circle-color': '#7c3aed',
|
|
'circle-radius': 6,
|
|
'circle-stroke-color': '#ffffff',
|
|
'circle-stroke-width': 2,
|
|
},
|
|
})
|
|
}, [selectionData, mapStyleReady])
|
|
|
|
useEffect(() => {
|
|
const map = mapRef.current
|
|
if (!map || !mapStyleReady || !map.isStyleLoaded()) {
|
|
return
|
|
}
|
|
|
|
const evidenceCollection = qaEvidenceData ?? EMPTY_FEATURE_COLLECTION
|
|
if (map.getSource('qa-evidence')) {
|
|
;(map.getSource('qa-evidence') as maplibregl.GeoJSONSource).setData(evidenceCollection)
|
|
return
|
|
}
|
|
|
|
map.addSource('qa-evidence', { type: 'geojson', data: evidenceCollection })
|
|
const evidenceColor = [
|
|
'match',
|
|
['get', 'qa_evidence_role'],
|
|
'match_candidate',
|
|
'#2563eb',
|
|
'match_reference',
|
|
'#0f766e',
|
|
'false_positive',
|
|
'#dc2626',
|
|
'false_negative',
|
|
'#d97706',
|
|
'#475569',
|
|
] as maplibregl.ExpressionSpecification
|
|
map.addLayer({
|
|
id: 'qa-evidence-fill',
|
|
type: 'fill',
|
|
source: 'qa-evidence',
|
|
filter: ['match', ['geometry-type'], ['Polygon', 'MultiPolygon'], true, false],
|
|
paint: {
|
|
'fill-color': evidenceColor,
|
|
'fill-opacity': 0.28,
|
|
},
|
|
})
|
|
map.addLayer({
|
|
id: 'qa-evidence-line',
|
|
type: 'line',
|
|
source: 'qa-evidence',
|
|
filter: ['match', ['geometry-type'], ['Polygon', 'MultiPolygon', 'LineString', 'MultiLineString'], true, false],
|
|
paint: {
|
|
'line-color': evidenceColor,
|
|
'line-width': [
|
|
'match',
|
|
['get', 'qa_evidence_role'],
|
|
'false_positive',
|
|
4,
|
|
'false_negative',
|
|
4,
|
|
3,
|
|
],
|
|
},
|
|
})
|
|
map.addLayer({
|
|
id: 'qa-evidence-circle',
|
|
type: 'circle',
|
|
source: 'qa-evidence',
|
|
filter: ['match', ['geometry-type'], ['Point', 'MultiPoint'], true, false],
|
|
paint: {
|
|
'circle-color': evidenceColor,
|
|
'circle-radius': 7,
|
|
'circle-stroke-color': '#ffffff',
|
|
'circle-stroke-width': 2,
|
|
},
|
|
})
|
|
}, [qaEvidenceData, mapStyleReady])
|
|
|
|
return (
|
|
<div
|
|
className="map-container"
|
|
ref={containerRef}
|
|
role="region"
|
|
aria-label="Interactieve kaart. Gebruik de kaartbediening om te zoomen en de tekenknop om een gebied te selecteren."
|
|
/>
|
|
)
|
|
}
|
|
|
|
export default GeoMap
|