diff --git a/.changeset/jolly-ravens-shop.md b/.changeset/jolly-ravens-shop.md new file mode 100644 index 000000000..5bbf1d864 --- /dev/null +++ b/.changeset/jolly-ravens-shop.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-app": minor +--- + +Add implementation of the AHN4 via the elevation layer in Atlas diff --git a/fdm-app/app/components/blocks/atlas/atlas-controls.tsx b/fdm-app/app/components/blocks/atlas/atlas-controls.tsx index e3400b754..3ab8e23cb 100644 --- a/fdm-app/app/components/blocks/atlas/atlas-controls.tsx +++ b/fdm-app/app/components/blocks/atlas/atlas-controls.tsx @@ -1,4 +1,4 @@ -import { Layers } from "lucide-react" +import { Layers, Mountain } from "lucide-react" import type { ControlPosition, Map as MapLibreMap } from "maplibre-gl" import { useEffect } from "react" import { createRoot, type Root } from "react-dom/client" @@ -19,6 +19,8 @@ type ControlsProps = { }) => void showFields?: boolean onToggleFields?: () => void + showElevation?: boolean + onToggleElevation?: () => void } export function Controls(props: ControlsProps) { @@ -36,6 +38,12 @@ export function Controls(props: ControlsProps) { onToggle={props.onToggleFields} /> )} + {props.showElevation !== undefined && props.onToggleElevation && ( + + )} void + labelActive: string + labelInactive: string + Icon: React.ElementType } -function FieldsButton({ showFields, onToggle }: FieldsButtonProps) { +function ControlButton({ active, onToggle, labelActive, labelInactive, Icon }: ButtonProps) { return ( ) } -class CustomFieldsControl implements IControl { +class CustomControl implements IControl { _map: MapLibreMap | undefined _container: HTMLDivElement | undefined _root: Root | undefined - _props: FieldsButtonProps + _props: ButtonProps - constructor(initialProps: FieldsButtonProps) { + constructor(initialProps: ButtonProps) { this._props = initialProps } @@ -92,7 +103,10 @@ class CustomFieldsControl implements IControl { onRemove(): void { if (this._root) { - this._root.unmount() + const root = this._root + setTimeout(() => { + root.unmount() + }, 0) this._root = undefined } this._container?.parentNode?.removeChild(this._container) @@ -104,14 +118,14 @@ class CustomFieldsControl implements IControl { return "top-right" } - updateProps(newProps: FieldsButtonProps) { + updateProps(newProps: ButtonProps) { this._props = newProps this._render() } _render() { if (this._root) { - this._root.render() + this._root.render() } } } @@ -125,14 +139,57 @@ function FieldsControl({ showFields: boolean onToggle: () => void }) { - const control = useControl( - () => new CustomFieldsControl({ showFields, onToggle }), + const control = useControl( + () => new CustomControl({ + active: showFields, + onToggle, + labelActive: "Verberg percelen", + labelInactive: "Toon percelen", + Icon: Layers + }), CONTROL_OPTIONS, ) useEffect(() => { - control.updateProps({ showFields, onToggle }) + control.updateProps({ + active: showFields, + onToggle, + labelActive: "Verberg percelen", + labelInactive: "Toon percelen", + Icon: Layers + }) }, [control, showFields, onToggle]) return null } + +function ElevationControl({ + showElevation, + onToggle, +}: { + showElevation: boolean + onToggle: () => void +}) { + const control = useControl( + () => new CustomControl({ + active: showElevation, + onToggle, + labelActive: "Verberg hoogtekaart", + labelInactive: "Toon hoogtekaart", + Icon: Mountain + }), + CONTROL_OPTIONS, + ) + + useEffect(() => { + control.updateProps({ + active: showElevation, + onToggle, + labelActive: "Verberg hoogtekaart", + labelInactive: "Toon hoogtekaart", + Icon: Mountain + }) + }, [control, showElevation, onToggle]) + + return null +} diff --git a/fdm-app/app/components/blocks/atlas/atlas-legend.tsx b/fdm-app/app/components/blocks/atlas/atlas-legend.tsx new file mode 100644 index 000000000..d67af11b7 --- /dev/null +++ b/fdm-app/app/components/blocks/atlas/atlas-legend.tsx @@ -0,0 +1,70 @@ +import { Card, CardContent } from "~/components/ui/card" +import { LoadingSpinner } from "~/components/custom/loadingspinner" + +interface ElevationLegendProps { + min?: number + max?: number + loading?: boolean + hoverValue?: number | null + showScale?: boolean + networkStatus?: "idle" | "loading" | "slow" | "error" + message?: string +} + +export function ElevationLegend({ min, max, loading, hoverValue, showScale = true, networkStatus, message }: ElevationLegendProps) { + return ( +
+ + +
+

+ Hoogte (AHN4) +

+ {loading && } +
+ + {networkStatus === "slow" && ( +
+ Trage verbinding... +
+ )} + + {networkStatus === "error" && ( +
+ Fout bij laden +
+ )} + + {message && ( +
+ {message} +
+ )} + + {showScale && ( +
+
+
Red) + background: "linear-gradient(to right, #5e4fa2, #3288bd, #66c2a5, #abdda4, #e6f598, #ffffbf, #fee08b, #fdae61, #f46d43, #d53e4f, #9e0142)" + }} + /> +
+
+ {min !== undefined ? `${min.toFixed(1)}m` : "Laag"} + {max !== undefined ? `${max.toFixed(1)}m` : "Hoog"} +
+ {hoverValue !== undefined && hoverValue !== null && ( +
+ Hoogte: {hoverValue.toFixed(2)} m NAP +
+ )} +
+ )} + + +
+ ) +} \ No newline at end of file diff --git a/fdm-app/app/components/blocks/header/atlas.tsx b/fdm-app/app/components/blocks/header/atlas.tsx index 51e2f8a81..cee2b2303 100644 --- a/fdm-app/app/components/blocks/header/atlas.tsx +++ b/fdm-app/app/components/blocks/header/atlas.tsx @@ -1,12 +1,25 @@ import { useCalendarStore } from "@/app/store/calendar" +import { ChevronDown } from "lucide-react" +import { useLocation, NavLink } from "react-router" import { BreadcrumbItem, BreadcrumbLink, BreadcrumbSeparator, + BreadcrumbPage, } from "~/components/ui/breadcrumb" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "~/components/ui/dropdown-menu" export function HeaderAtlas({ b_id_farm }: { b_id_farm: string | undefined }) { const calendar = useCalendarStore((state) => state.calendar) + const location = useLocation() + + const isElevation = location.pathname.includes("/elevation") + const currentName = isElevation ? "Hoogtekaart" : "Gewaspercelen" return ( <> @@ -18,11 +31,24 @@ export function HeaderAtlas({ b_id_farm }: { b_id_farm: string | undefined }) { - - Percelen - + + + {currentName} + + + + + + Gewaspercelen + + + + + Hoogtekaart + + + + ) diff --git a/fdm-app/app/components/blocks/sidebar/apps.tsx b/fdm-app/app/components/blocks/sidebar/apps.tsx index d747db21e..d97ce6092 100644 --- a/fdm-app/app/components/blocks/sidebar/apps.tsx +++ b/fdm-app/app/components/blocks/sidebar/apps.tsx @@ -41,12 +41,20 @@ export function SidebarApps() { location.pathname === "/farm" || location.pathname === "/farm/" let atlasLink: string | undefined + let atlasFieldsLink: string | undefined + let atlasElevationLink: string | undefined if (isCreateFarmWizard) { atlasLink = undefined + atlasFieldsLink = undefined + atlasElevationLink = undefined } else if (farmId) { atlasLink = `/farm/${farmId}/${selectedCalendar}/atlas` + atlasFieldsLink = `/farm/${farmId}/${selectedCalendar}/atlas/fields` + atlasElevationLink = `/farm/${farmId}/${selectedCalendar}/atlas/elevation` } else { atlasLink = `/farm/undefined/${selectedCalendar}/atlas` + atlasFieldsLink = `/farm/undefined/${selectedCalendar}/atlas/fields` + atlasElevationLink = `/farm/undefined/${selectedCalendar}/atlas/elevation` } let nitrogenBalanceLink: string | undefined @@ -89,29 +97,65 @@ export function SidebarApps() { Apps - - {atlasLink ? ( - - - - Atlas - - - ) : ( - - - - Atlas - - - )} - + + + {atlasLink ? ( + + + + Atlas + + + + + ) : ( + + + + Atlas + + + )} + + + + {atlasFieldsLink ? ( + + + Gewaspercelen + + + ) : null} + + + {atlasElevationLink ? ( + + + Hoogtekaart + + + ) : null} + + + + + y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi + if (intersect) inside = !inside + } + return inside +} + +// Helper: Check if polygon intersects polygon (simple AABB check for index speed) +// For now, we just check if any point of tile is in view or view in tile +// Simpler: Convert Viewport to RD Polygon, check intersection with Tile Polygon (also RD). +function polygonIntersectsPolygon( + poly1: [number, number][], + poly2: [number, number][], +) { + // Simplified: Check if any point of poly1 is in poly2 OR any point of poly2 is in poly1 + // This is not 100% robust for crossing polygons but good enough for tiles + for (const p of poly1) if (isPointInPolygon(p, poly2)) return true + for (const p of poly2) if (isPointInPolygon(p, poly1)) return true + return false +} + +interface ActiveTile { + id: string + url: string + cogUrl: string | null + cogUrlHillshade: string | null +} + // Meta export const meta: MetaFunction = () => { return [ - { title: `Hoogte - Kaart | ${clientConfig.name}` }, + { title: `Hoogte - Atlas | ${clientConfig.name}` }, { name: "description", content: "Bekijk hoogtegegevens op de kaart.", @@ -25,82 +102,572 @@ export const meta: MetaFunction = () => { /** * Loads farm field data for the elevation feature. - * - * This asynchronous function checks for the presence of a farm ID in the route parameters. It retrieves the user session, fetches the fields associated with the specified farm, and maps them to a GeoJSON FeatureCollection. Errors during these processes, such as a missing farm ID or data retrieval issues, are caught and rethrown. - * @param {LoaderFunctionArgs} args - The arguments provided by the loader, including the request and parameters. - * @returns An object containing the GeoJSON FeatureCollection of farm fields */ export async function loader({ request, params }: LoaderFunctionArgs) { try { - // Get the farm id const b_id_farm = params.b_id_farm - if (!b_id_farm) { - throw data("Farm ID is required", { - status: 400, - statusText: "Farm ID is required", - }) - } - // Get the session const session = await getSession(request) - - // Get timeframe from calendar store + const calendar = getCalendar(params) const timeframe = getTimeframe(params) // Get the fields of the farm - const fields = await getFields( - fdm, - session.principal_id, - b_id_farm, - timeframe, - ) - const features = fields.map((field) => { - const feature = { - type: "Feature", - properties: { - b_id: field.b_id, - b_name: field.b_name, - b_area: Math.round(field.b_area * 10) / 10, - }, - geometry: field.b_geometry, - } - return feature - }) + let featureCollection: FeatureCollection | undefined + if (b_id_farm && b_id_farm !== "undefined") { + const fields = await getFields( + fdm, + session.principal_id, + b_id_farm, + timeframe, + ) + const features = fields.map((field) => { + const feature = { + type: "Feature" as const, + properties: { + b_id: field.b_id, + b_name: field.b_name, + b_area: Math.round(field.b_area * 10) / 10, + b_lu_name: field.b_lu_name, + b_id_source: field.b_id_source, + }, + geometry: field.b_geometry, + } + return feature + }) - const featureCollection = { - type: "FeatureCollection", - features: features, + featureCollection = { + type: "FeatureCollection", + features: features, + } } - // Return user information from loader + const mapStyle = getMapStyle("satellite") + return { fields: featureCollection, + mapStyle, + calendar, } } catch (error) { throw handleLoaderError(error) } } -/** - * Renders a placeholder UI for the farm elevation feature. - * - * This component displays a message informing the user that the elevation map is not yet available and provides a button that navigates to the field map. - */ export default function FarmAtlasElevationBlock() { + const loaderData = useLoaderData() + const fields = loaderData.fields + const mapStyle = loaderData.mapStyle + + const mapRef = useRef(null) + + // State + const [indexData, setIndexData] = useState(null) + const [activeTiles, setActiveTiles] = useState([]) + const [isUpdating, setIsUpdating] = useState(false) + const [legendMin, setLegendMin] = useState(-5) + const [legendMax, setLegendMax] = useState(50) + const [hoverElevation, setHoverElevation] = useState(null) + const [showFields, setShowFields] = useState(true) + const [showElevation, setShowElevation] = useState(true) + const [networkStatus, setNetworkStatus] = useState< + "idle" | "loading" | "slow" | "error" + >("idle") + + const fieldsSavedId = "fieldsSaved" + const fieldsSavedStyle = getFieldsStyle(fieldsSavedId) + const fieldsSavedOutlineStyle = getFieldsStyle("fieldsSavedOutline") + const layerLayout = { visibility: showFields ? "visible" : "none" } as const + + const onToggleElevation = useCallback(() => { + setShowElevation((prev) => !prev) + }, []) + + // ViewState logic + const initialViewState = getViewState(fields) + const [viewState, setViewState] = useState(() => { + if (typeof window !== "undefined") { + const savedViewState = sessionStorage.getItem("mapViewState") + if (savedViewState) { + try { + return JSON.parse(savedViewState) + } catch { + sessionStorage.removeItem("mapViewState") + } + } + } + return initialViewState as ViewState + }) + + const onViewportChange = useCallback((event: ViewStateChangeEvent) => { + setViewState(event.viewState) + }, []) + + // Save viewState + const isFirstRender = useRef(true) + useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false + return + } + sessionStorage.setItem("mapViewState", JSON.stringify(viewState)) + }, [viewState]) + + // Fetch COG Index once + useEffect(() => { + async function fetchIndex() { + const cacheKey = "ahn_kaartbladindex_v1" + setNetworkStatus("loading") + try { + // Try cache + if (typeof localStorage !== "undefined") { + const cached = localStorage.getItem(cacheKey) + if (cached) { + try { + const { timestamp, data } = JSON.parse(cached) + // Cache for 7 days + if ( + Date.now() - timestamp < + 7 * 24 * 60 * 60 * 1000 + ) { + setIndexData(data) + setNetworkStatus("idle") + return + } + } catch { + localStorage.removeItem(cacheKey) + } + } + } + + const response = await fetch( + "https://service.pdok.nl/rws/ahn/atom/downloads/dtm_05m/kaartbladindex.json", + ) + if (!response.ok) throw new Error("Failed to fetch COG index") + const data = (await response.json()) as FeatureCollection + setIndexData(data) + setNetworkStatus("idle") + + if (typeof localStorage !== "undefined") { + try { + localStorage.setItem( + cacheKey, + JSON.stringify({ timestamp: Date.now(), data }), + ) + } catch (e) { + console.warn("Cache storage failed", e) + } + } + } catch (e) { + console.error("Error fetching COG index:", e) + setNetworkStatus("error") + } + } + fetchIndex() + }, []) + + const updateId = useRef(0) + + // Function to update visible tiles + const updateVisibleTiles = useCallback(async () => { + if (!mapRef.current || !indexData) return + + const bounds = mapRef.current.getBounds() + const zoom = mapRef.current.getZoom() + + // If zoomed out, clear active tiles to save resources (WMS will take over) + if (zoom < 13) { + if (activeTiles.length > 0) { + setActiveTiles([]) + } + return + } + + const currentId = ++updateId.current + setIsUpdating(true) + setNetworkStatus("loading") + + // Detect slow network + const slowTimer = setTimeout(() => { + if (updateId.current === currentId) { + setNetworkStatus("slow") + } + }, 2000) + + const sw = bounds.getSouthWest() + const ne = bounds.getNorthEast() + const nw = bounds.getNorthWest() + const se = bounds.getSouthEast() + + // Convert viewport corners to RD (EPSG:28992) + // We catch projection errors if points are outside valid range + try { + const rdCoords = [ + proj4("EPSG:28992").forward([nw.lng, nw.lat]), + proj4("EPSG:28992").forward([ne.lng, ne.lat]), + proj4("EPSG:28992").forward([se.lng, se.lat]), + proj4("EPSG:28992").forward([sw.lng, sw.lat]), + ] as [number, number][] + + // Find intersecting tiles + // Optimization: limit to e.g. 24 tiles to avoid overload + const visibleFeatures = indexData.features + .filter((f) => { + if (!f.geometry || f.geometry.type !== "Polygon") + return false + const ring = (f.geometry as any).coordinates[0] + return polygonIntersectsPolygon(rdCoords, ring) + }) + .slice(0, 24) + + // Calculate global min/max for the viewport by sampling + const samplePoints: { lng: number; lat: number }[] = [] + const gridSize = 3 + for (let i = 0; i <= gridSize; i++) { + for (let j = 0; j <= gridSize; j++) { + const lng = sw.lng + (ne.lng - sw.lng) * (i / gridSize) + const lat = sw.lat + (ne.lat - sw.lat) * (j / gridSize) + samplePoints.push({ lng, lat }) + } + } + + let min = 1000 + let max = -1000 + + // Gather values for samples + const values = await Promise.all( + samplePoints.map(async (p) => { + try { + const rdP = proj4("EPSG:28992").forward([ + p.lng, + p.lat, + ]) as [number, number] + // Find which tile contains this point + const feature = visibleFeatures.find((f) => { + if (!f.geometry || f.geometry.type !== "Polygon") + return false + const ring = (f.geometry as any).coordinates[0] + return isPointInPolygon(rdP, ring) + }) + if (feature?.properties) { + const url = + feature.properties.url || + feature.properties.href || + feature.properties.download_url + if (url) { + // Requesting location value + const vals = await locationValues(url, { + longitude: p.lng, + latitude: p.lat, + }) + if ( + vals && + vals.length > 0 && + !Number.isNaN(vals[0]) && + vals[0] > -100 && + vals[0] < 1000 + ) { + return vals[0] + } + } + } + } catch { + // Ignore errors for individual points + } + return null + }), + ) + + if (updateId.current !== currentId) return + + const validValues = values.filter((v) => v !== null) as number[] + if (validValues.length > 0) { + min = Math.min(...validValues) + max = Math.max(...validValues) + } else { + min = -5 + max = 50 + } + + // Ensure minimum contrast + if (max - min < 1) { + min -= 0.5 + max += 0.5 + } + + // Pad range slightly + const range = max - min + min -= range * 0.05 + max += range * 0.05 + + setLegendMin(min) + setLegendMax(max) + + // Format for color scale + const colorParam = `#color:BrewerSpectral11,${min},${max},-c` + + const newTiles: ActiveTile[] = [] + for (const feature of visibleFeatures) { + if (!feature.properties) continue + const url = + feature.properties.url || + feature.properties.href || + feature.properties.download_url + + if (!url) continue + const id = feature.properties.kaartbladNr || url + + newTiles.push({ + id, + url, + cogUrl: `cog://${url}${colorParam}`, + cogUrlHillshade: `cog://${url}#dem`, + }) + } + + setActiveTiles(newTiles) + setNetworkStatus("idle") + } catch (e) { + console.error("Error updating visible tiles:", e) + if (updateId.current === currentId) { + setNetworkStatus("error") + } + } finally { + if (updateId.current === currentId) { + setIsUpdating(false) + } + clearTimeout(slowTimer) + } + }, [indexData, activeTiles]) + + // Throttle updates + const updateRef = useRef(updateVisibleTiles) + useEffect(() => { + updateRef.current = updateVisibleTiles + }, [updateVisibleTiles]) + + const throttledUpdate = useMemo( + () => + throttle(() => updateRef.current(), 500, { + leading: true, + trailing: true, + }), + [], + ) + + // Initial update when map loads or index loads + useEffect(() => { + const timer = setTimeout(() => { + throttledUpdate() + }, 1000) + return () => clearTimeout(timer) + }, [throttledUpdate]) + + // Refs for state accessible in throttled functions + const stateRef = useRef({ indexData, activeTiles }) + useEffect(() => { + stateRef.current = { indexData, activeTiles } + }, [indexData, activeTiles]) + + // Handle hover to show elevation value + const handleMouseMove = useMemo( + () => + throttle(async (event: MapLayerMouseEvent) => { + const { indexData, activeTiles } = stateRef.current + + if (!mapRef.current || mapRef.current.getZoom() < 13) { + setHoverElevation(null) + return + } + + if (!indexData || activeTiles.length === 0) return + + const { lng, lat } = event.lngLat + + try { + const rdP = proj4("EPSG:28992").forward([lng, lat]) as [ + number, + number, + ] + + const feature = indexData.features.find((f) => { + if (!f.geometry || f.geometry.type !== "Polygon") + return false + const ring = (f.geometry as any).coordinates[0] + return isPointInPolygon(rdP, ring) + }) + + if (feature?.properties) { + const url = + feature.properties.url || + feature.properties.href || + feature.properties.download_url + if (url) { + const values = await locationValues(url, { + longitude: lng, + latitude: lat, + }) + if ( + values && + values.length > 0 && + !Number.isNaN(values[0]) + ) { + setHoverElevation(values[0]) + return + } + } + } + setHoverElevation(null) + } catch (e) { + setHoverElevation(null) + } + }, 200), + [], + ) + return ( -
-
-

- Helaas, de hoogtekaart is nog niet beschikbaar :( -

-

- We proberen de hoogtekaart binnenkort toe te voegen. Hou de - website in de gaten. -

-
- +
+ + + setViewState((currentViewState) => ({ + ...currentViewState, + longitude, + latitude, + zoom, + })) + } + showFields={showFields} + onToggleFields={() => setShowFields(!showFields)} + showElevation={showElevation} + onToggleElevation={onToggleElevation} + /> + + {/* WMS Overview Layer (Zoom < 13) */} + {showElevation && ( + + + + )} + + {/* Render Active Tiles (Zoom >= 13) */} + {showElevation && + activeTiles.map((tile) => ( + + + + + + + + + ))} + + {/* Fields Overlay (Saved Fields) */} + {fields && ( + + {/* Outline Layer - Visual */} + + {/* Fill Layer - Invisible but Clickable/Hoverable */} + + + )} + +
+ = 13 && showElevation} + networkStatus={networkStatus} + message={ + showElevation && viewState.zoom < 13 + ? "Zoom in voor meer detail" + : undefined + } + /> +
+ +
+
+
) } diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.fields._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.fields._index.tsx index a4c66dc48..8ec49002f 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.fields._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.fields._index.tsx @@ -28,7 +28,7 @@ import { fdm } from "~/lib/fdm.server" export const meta: MetaFunction = () => { return [ - { title: `Percelen - Kaart | ${clientConfig.name}` }, + { title: `Percelen - Atlas | ${clientConfig.name}` }, { name: "description", content: diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.tsx index 752724012..7ca36b82d 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.tsx @@ -21,7 +21,7 @@ import { fdm } from "~/lib/fdm.server" // Meta export const meta: MetaFunction = () => { return [ - { title: `Kaarten | ${clientConfig.name}` }, + { title: `Atlas | ${clientConfig.name}` }, { name: "description", content: "Bekijk informatie op de kaart.", diff --git a/fdm-app/package.json b/fdm-app/package.json index 7d7b9a36b..4529fcfea 100644 --- a/fdm-app/package.json +++ b/fdm-app/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@date-fns/tz": "^1.4.1", + "@geomatico/maplibre-cog-protocol": "github:SvenVw/maplibre-cog-protocol#add-prepare", "@hookform/resolvers": "^5.2.2", "@lucide/lab": "^0.1.2", "@mapbox/geojson-extent": "^1.0.1", @@ -40,6 +41,7 @@ "@svenvw/fdm-data": "workspace:*", "@tailwindcss/vite": "^4.1.17", "@tanstack/react-table": "^8.21.3", + "@turf/boolean-intersects": "^7.3.1", "@turf/centroid": "^7.3.1", "@turf/turf": "^7.3.1", "better-auth": "catalog:", diff --git a/fdm-app/vite.config.ts b/fdm-app/vite.config.ts index 1df0ef858..39ae0085b 100644 --- a/fdm-app/vite.config.ts +++ b/fdm-app/vite.config.ts @@ -31,7 +31,7 @@ export default defineConfig((env) => { ].filter(Boolean), envPrefix: "PUBLIC_", ssr: { - noExternal: ["posthog-js", "posthog-js/react"], + noExternal: ["posthog-js", "posthog-js/react", "@geomatico/maplibre-cog-protocol"], }, build: { sourcemap: true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b19455776..617eea9cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -79,6 +79,9 @@ importers: '@date-fns/tz': specifier: ^1.4.1 version: 1.4.1 + '@geomatico/maplibre-cog-protocol': + specifier: github:SvenVw/maplibre-cog-protocol#add-prepare + version: https://codeload.github.com/SvenVw/maplibre-cog-protocol/tar.gz/fdb1f2505071f6bd3dc94fe07024d3ddfe826904(maplibre-gl@5.14.0) '@hookform/resolvers': specifier: ^5.2.2 version: 5.2.2(react-hook-form@7.68.0(react@19.2.1)) @@ -154,6 +157,9 @@ importers: '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@turf/boolean-intersects': + specifier: ^7.3.1 + version: 7.3.1 '@turf/centroid': specifier: ^7.3.1 version: 7.3.1 @@ -2224,6 +2230,12 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@geomatico/maplibre-cog-protocol@https://codeload.github.com/SvenVw/maplibre-cog-protocol/tar.gz/fdb1f2505071f6bd3dc94fe07024d3ddfe826904': + resolution: {tarball: https://codeload.github.com/SvenVw/maplibre-cog-protocol/tar.gz/fdb1f2505071f6bd3dc94fe07024d3ddfe826904} + version: 0.8.0 + peerDependencies: + maplibre-gl: ^4.5.0 || ^5.0.0 + '@gerrit0/mini-shiki@3.19.0': resolution: {integrity: sha512-ZSlWfLvr8Nl0T4iA3FF/8VH8HivYF82xQts2DY0tJxZd4wtXJ8AA0nmdW9lmO4hlrh3f9xNwEPtOgqETPqKwDA==} @@ -2359,6 +2371,10 @@ packages: '@mapbox/point-geometry@1.1.0': resolution: {integrity: sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==} + '@mapbox/sphericalmercator@1.2.0': + resolution: {integrity: sha512-ZTOuuwGuMOJN+HEmG/68bSEw15HHaMWmQ5gdTsWdWsjDe56K1kGvLOK6bOSC8gWgIvEO0w6un/2Gvv1q5hJSkQ==} + hasBin: true + '@mapbox/tiny-sdf@2.0.7': resolution: {integrity: sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==} @@ -8857,6 +8873,10 @@ packages: resolution: {integrity: sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==} engines: {node: '>=12'} + quick-lru@7.3.0: + resolution: {integrity: sha512-k9lSsjl36EJdK7I06v7APZCbyGT2vMTsYSRX1Q2nbYmnkBqgUhRkAuzH08Ciotteu/PLJmIF2+tti7o3C/ts2g==} + engines: {node: '>=18'} + quickselect@1.1.1: resolution: {integrity: sha512-qN0Gqdw4c4KGPsBOQafj6yj/PA6c/L63f6CaZ/DCF/xF4Esu3jVmKLUDYxghFx8Kb/O7y9tI7x2RjTSXwdK1iQ==} @@ -11394,7 +11414,7 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@better-auth/core@1.4.5(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.4(zod@4.1.13))(jose@6.1.3)(kysely@0.28.8)(nanostores@1.1.0)': + '@better-auth/core@1.4.5(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.4(zod@3.25.76))(jose@6.1.3)(kysely@0.28.8)(nanostores@1.1.0)': dependencies: '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.18 @@ -11405,9 +11425,9 @@ snapshots: nanostores: 1.1.0 zod: 4.1.13 - '@better-auth/telemetry@1.4.5(@better-auth/core@1.4.5(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.4(zod@4.1.13))(jose@6.1.3)(kysely@0.28.8)(nanostores@1.1.0))': + '@better-auth/telemetry@1.4.5(@better-auth/core@1.4.5(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.4(zod@3.25.76))(jose@6.1.3)(kysely@0.28.8)(nanostores@1.1.0))': dependencies: - '@better-auth/core': 1.4.5(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.4(zod@4.1.13))(jose@6.1.3)(kysely@0.28.8)(nanostores@1.1.0) + '@better-auth/core': 1.4.5(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.4(zod@3.25.76))(jose@6.1.3)(kysely@0.28.8)(nanostores@1.1.0) '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.18 @@ -12894,6 +12914,15 @@ snapshots: '@floating-ui/utils@0.2.10': {} + '@geomatico/maplibre-cog-protocol@https://codeload.github.com/SvenVw/maplibre-cog-protocol/tar.gz/fdb1f2505071f6bd3dc94fe07024d3ddfe826904(maplibre-gl@5.14.0)': + dependencies: + '@mapbox/sphericalmercator': 1.2.0 + d3-scale: 4.0.2 + geotiff: 2.1.3 + maplibre-gl: 5.14.0 + proj4: 2.20.2 + quick-lru: 7.3.0 + '@gerrit0/mini-shiki@3.19.0': dependencies: '@shikijs/engine-oniguruma': 3.19.0 @@ -13053,6 +13082,8 @@ snapshots: '@mapbox/point-geometry@1.1.0': {} + '@mapbox/sphericalmercator@1.2.0': {} + '@mapbox/tiny-sdf@2.0.7': {} '@mapbox/unitbezier@0.0.1': {} @@ -17000,8 +17031,8 @@ snapshots: better-auth@1.4.5(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: - '@better-auth/core': 1.4.5(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.4(zod@4.1.13))(jose@6.1.3)(kysely@0.28.8)(nanostores@1.1.0) - '@better-auth/telemetry': 1.4.5(@better-auth/core@1.4.5(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.4(zod@4.1.13))(jose@6.1.3)(kysely@0.28.8)(nanostores@1.1.0)) + '@better-auth/core': 1.4.5(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.4(zod@3.25.76))(jose@6.1.3)(kysely@0.28.8)(nanostores@1.1.0) + '@better-auth/telemetry': 1.4.5(@better-auth/core@1.4.5(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.4(zod@3.25.76))(jose@6.1.3)(kysely@0.28.8)(nanostores@1.1.0)) '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.18 '@noble/ciphers': 2.0.1 @@ -20922,6 +20953,8 @@ snapshots: quick-lru@6.1.2: {} + quick-lru@7.3.0: {} + quickselect@1.1.1: {} quickselect@2.0.0: {}