import { featureCollectionBounds } from '../../lib/geojsonBounds' import type { ProjectRead, VectorSelectionBBox, VectorSelectionMetric, VectorSelectionResponse, } from '../../types' import { FLANDERS_WORKSPACE_LABEL, FLANDERS_WORKSPACE_PROJECT_NAME, } from '../../config/primaryFocus' const MOL_PROJECT_NAME = 'Mol Municipality Workbench' const KEMPEN_PROJECT_NAME = 'Kempen Regional Workbench' export function selectedAreaCoverageZones(areaName: string | null | undefined): string[] | null { const normalized = String(areaName ?? '').toLowerCase() if (!normalized) return null if (normalized.includes('coast land-sea')) return ['flanders', 'belgian_north_sea'] if (normalized.includes('language boundary')) return ['flanders', 'wallonia'] if (normalized.includes('north sea')) { return ['belgian_north_sea', 'territorial_sea', 'exclusive_economic_zone', 'continental_shelf'] } if (normalized.includes('territorial sea')) return ['territorial_sea'] if (normalized.includes('exclusive economic zone')) return ['exclusive_economic_zone'] if (normalized.includes('continental shelf')) return ['continental_shelf'] if (normalized.includes('brussels')) return ['brussels'] if (normalized.includes('wallonia') || normalized.includes('ardennes')) return ['wallonia'] if (normalized.includes('flanders') || normalized.includes('mol') || normalized.includes('kempen')) return ['flanders'] if (normalized.includes('belgium land')) return ['belgium', 'flanders', 'wallonia', 'brussels'] return null } export function productCoversZones(productZones: string[], selectedZones: string[] | null): boolean { return selectedZones === null || selectedZones.some((zone) => productZones.includes(zone)) } export function isSelectionBoundedDataset( sourceMetadata: Record | null | undefined, ): boolean { return sourceMetadata?.['geometry_clipped_to_selection'] === true && Array.isArray(sourceMetadata?.['bbox_epsg4326']) && sourceMetadata['bbox_epsg4326'].length === 4 } export function operationalScopeProjectLabel(project: ProjectRead): string { if (project.name === MOL_PROJECT_NAME) { return 'Mol' } if (project.name === KEMPEN_PROJECT_NAME) { return 'Kempen (28 gemeenten)' } if (project.name === FLANDERS_WORKSPACE_PROJECT_NAME) { return FLANDERS_WORKSPACE_LABEL } return project.name } export function selectionAreaSquareMetres(bbox: VectorSelectionBBox | null): number | null { if (!bbox) { return null } const middleLatitudeRadians = ((bbox.min_y + bbox.max_y) / 2) * (Math.PI / 180) const widthMetres = (bbox.max_x - bbox.min_x) * 111_320 * Math.cos(middleLatitudeRadians) const heightMetres = (bbox.max_y - bbox.min_y) * 110_574 return Math.max(0, widthMetres * heightMetres) } export function bboxesEqual(left: VectorSelectionBBox | null, right: VectorSelectionBBox | null): boolean { if (!left || !right) { return false } const tolerance = 1e-9 return ( Math.abs(left.min_x - right.min_x) < tolerance && Math.abs(left.min_y - right.min_y) < tolerance && Math.abs(left.max_x - right.max_x) < tolerance && Math.abs(left.max_y - right.max_y) < tolerance ) } export function formatArea(areaSquareMetres: number | null): string { if (areaSquareMetres === null) { return 'Nog niet geselecteerd' } if (areaSquareMetres >= 1_000_000) { return `${(areaSquareMetres / 1_000_000).toLocaleString('nl-BE', { maximumFractionDigits: 2 })} km2` } return `${(areaSquareMetres / 10_000).toLocaleString('nl-BE', { maximumFractionDigits: 2 })} ha` } export function resultCountLabel(result: VectorSelectionResponse): string { const total = result.total_feature_count ?? result.feature_count return result.truncated && result.total_feature_count == null ? `${result.feature_count.toLocaleString('nl-BE')}+` : total.toLocaleString('nl-BE') } export function resultMetricLabel(result: VectorSelectionResponse): string { if (!result.summary) { return resultCountLabel(result) } const maximumFractionDigits = result.summary.metric_unit === 'inwoners' ? 0 : 2 return `${result.summary.metric_value.toLocaleString('nl-BE', { maximumFractionDigits })} ${result.summary.metric_unit}` } export function selectionMetricLabel(metric: VectorSelectionMetric): string { const maximumFractionDigits = metric.metric_unit === 'inwoners' || metric.metric_unit === 'objecten' ? 0 : 2 return `${metric.metric_value.toLocaleString('nl-BE', { maximumFractionDigits })} ${metric.metric_unit}` } export function formatTemporalMetric(value: number, unit: string): string { const maximumFractionDigits = unit === 'inwoners' || unit === 'objecten' ? 0 : 2 return `${value.toLocaleString('nl-BE', { maximumFractionDigits })} ${unit}` } export function readablePropertyName(value: string): string { return value.replace(/_/g, ' ').replace(/\b\w/g, (character) => character.toUpperCase()) } function collectGeometryPoints(geometry: GeoJSON.Geometry | null | undefined): Array<[number, number]> { const points: Array<[number, number]> = [] const walk = (coords: unknown) => { if (!Array.isArray(coords)) { return } if (coords.length >= 2 && typeof coords[0] === 'number' && typeof coords[1] === 'number') { points.push([coords[0], coords[1]]) return } for (const item of coords) { walk(item) } } if ('coordinates' in (geometry ?? {})) { walk((geometry as GeoJSON.Geometry & { coordinates: unknown }).coordinates) } return points } function formatCoordinate(value: number): string { return Number.isFinite(value) ? value.toFixed(6) : 'n.v.t.' } export function getFeatureGeometrySummary(feature: GeoJSON.Feature | null) { const points = collectGeometryPoints(feature?.geometry) if (!feature?.geometry || points.length === 0) { return { bboxLabel: 'n.v.t.', coordinateCount: 0, geometryType: feature?.geometry?.type ?? 'geen', } } const xs = points.map((point) => point[0]) const ys = points.map((point) => point[1]) const bboxLabel = `${formatCoordinate(Math.min(...xs))}, ${formatCoordinate(Math.min(...ys))} -> ${formatCoordinate( Math.max(...xs), )}, ${formatCoordinate(Math.max(...ys))}` return { bboxLabel, coordinateCount: points.length, geometryType: feature.geometry.type, } } export function getFeatureCollectionBBox(collection: GeoJSON.FeatureCollection | null): VectorSelectionBBox | null { const bounds = featureCollectionBounds(collection) if (!bounds) { return null } return { min_x: bounds.minX, min_y: bounds.minY, max_x: bounds.maxX, max_y: bounds.maxY, crs: 'EPSG:4326', } } export function getFeatureBBox(feature: GeoJSON.Feature | null): VectorSelectionBBox | null { const points = collectGeometryPoints(feature?.geometry) if (points.length === 0) { return null } const xs = points.map((point) => point[0]) const ys = points.map((point) => point[1]) return { min_x: Math.min(...xs), min_y: Math.min(...ys), max_x: Math.max(...xs), max_y: Math.max(...ys), crs: 'EPSG:4326', } } export function normalizeBboxFromCorners( first: [number, number], second: [number, number], ): VectorSelectionBBox { return { min_x: Math.min(first[0], second[0]), min_y: Math.min(first[1], second[1]), max_x: Math.max(first[0], second[0]), max_y: Math.max(first[1], second[1]), crs: 'EPSG:4326', } } export function formatBboxLabel(bbox: VectorSelectionBBox | null): string { if (!bbox) { return 'n.v.t.' } return `${formatCoordinate(bbox.min_x)}, ${formatCoordinate(bbox.min_y)} -> ${formatCoordinate(bbox.max_x)}, ${formatCoordinate(bbox.max_y)}` } export function formatPercentage(value: number | null | undefined): string { return typeof value === 'number' && Number.isFinite(value) ? `${(value * 100).toLocaleString('nl-BE', { maximumFractionDigits: 1 })}%` : 'n.v.t.' } export function bboxToInputState(bbox: VectorSelectionBBox | null) { return { min_x: bbox ? String(bbox.min_x) : '', min_y: bbox ? String(bbox.min_y) : '', max_x: bbox ? String(bbox.max_x) : '', max_y: bbox ? String(bbox.max_y) : '', } } export function parseBboxInput(input: ReturnType): VectorSelectionBBox | null { const min_x = Number(input.min_x) const min_y = Number(input.min_y) const max_x = Number(input.max_x) const max_y = Number(input.max_y) if (![min_x, min_y, max_x, max_y].every(Number.isFinite) || min_x >= max_x || min_y >= max_y) { return null } return { min_x, min_y, max_x, max_y, crs: 'EPSG:4326' } } export function selectedFeatureCollection(feature: GeoJSON.Feature): GeoJSON.FeatureCollection { return { type: 'FeatureCollection', features: [feature], } } export function safeFileStem(value: unknown): string { const stem = String(value ?? 'selected-feature') .trim() .toLowerCase() .replace(/[^a-z0-9._-]+/g, '-') .replace(/^-+|-+$/g, '') return stem || 'selected-feature' } function fallbackCopyText(text: string): void { const textarea = document.createElement('textarea') textarea.value = text textarea.setAttribute('readonly', 'true') textarea.style.position = 'fixed' textarea.style.left = '-9999px' document.body.appendChild(textarea) textarea.select() document.execCommand('copy') document.body.removeChild(textarea) } export function copyText(text: string): void { if (navigator.clipboard?.writeText) { void navigator.clipboard.writeText(text).catch(() => fallbackCopyText(text)) return } fallbackCopyText(text) } export function downloadJsonFile( filename: string, payload: unknown, contentType = 'application/json', ): void { const blob = new Blob([JSON.stringify(payload, null, 2)], { type: contentType }) const url = URL.createObjectURL(blob) const link = document.createElement('a') link.href = url link.download = filename document.body.appendChild(link) link.click() document.body.removeChild(link) URL.revokeObjectURL(url) }