-
-
- Helaas, de hoogtekaart is nog niet beschikbaar :(
-
-
- We proberen de hoogtekaart binnenkort toe te voegen. Hou de
- website in de gaten.
-
-
-
+
+ {isLoadingCog && (
+
+
+
+ Hoogtekaart laden...
+
+
+ )}
+
+
+
+ setViewState((currentViewState) => ({
+ ...currentViewState,
+ longitude,
+ latitude,
+ zoom,
+ }))
+ }
+ />
+
+ {/* Render Active Tiles */}
+ {activeTiles.map((tile) => (
+
+
+
+ ))}
+
+
+
)
}
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 dc39724ad..164fa6333 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: file:C:/Users/sven.verweij/Applications/packages/maplibre-cog-protocol
+ version: file:../packages/maplibre-cog-protocol(maplibre-gl@5.13.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,11 @@ packages:
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
+ '@geomatico/maplibre-cog-protocol@file:../packages/maplibre-cog-protocol':
+ resolution: {directory: ../packages/maplibre-cog-protocol, type: directory}
+ peerDependencies:
+ maplibre-gl: ^4.5.0 || ^5.0.0
+
'@gerrit0/mini-shiki@3.15.0':
resolution: {integrity: sha512-L5IHdZIDa4bG4yJaOzfasOH/o22MCesY0mx+n6VATbaiCtMeR59pdRqYk4bEiQkIHfxsHPNgdi7VJlZb2FhdMQ==}
@@ -2362,6 +2373,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==}
@@ -8871,6 +8886,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==}
@@ -12958,6 +12977,15 @@ snapshots:
'@floating-ui/utils@0.2.10': {}
+ '@geomatico/maplibre-cog-protocol@file:../packages/maplibre-cog-protocol(maplibre-gl@5.13.0)':
+ dependencies:
+ '@mapbox/sphericalmercator': 1.2.0
+ d3-scale: 4.0.2
+ geotiff: 2.1.3
+ maplibre-gl: 5.13.0
+ proj4: 2.20.2
+ quick-lru: 7.3.0
+
'@gerrit0/mini-shiki@3.15.0':
dependencies:
'@shikijs/engine-oniguruma': 3.15.0
@@ -13120,6 +13148,8 @@ snapshots:
'@mapbox/point-geometry@1.1.0': {}
+ '@mapbox/sphericalmercator@1.2.0': {}
+
'@mapbox/tiny-sdf@2.0.7': {}
'@mapbox/unitbezier@0.0.1': {}
@@ -21027,6 +21057,8 @@ snapshots:
quick-lru@6.1.2: {}
+ quick-lru@7.3.0: {}
+
quickselect@1.1.1: {}
quickselect@2.0.0: {}
From 0faed6b494107bb974fa17c046d9e8c7b1b4ee47 Mon Sep 17 00:00:00 2001
From: Sven Verweij <37927107+SvenVw@users.noreply.github.com>
Date: Fri, 5 Dec 2025 14:42:55 +0100
Subject: [PATCH 02/13] feat: improve elevation map
---
.../components/blocks/atlas/atlas-legend.tsx | 43 +++--
...m.$b_id_farm.$calendar.atlas.elevation.tsx | 166 ++++++++++++++----
2 files changed, 158 insertions(+), 51 deletions(-)
diff --git a/fdm-app/app/components/blocks/atlas/atlas-legend.tsx b/fdm-app/app/components/blocks/atlas/atlas-legend.tsx
index e3e4a90b0..1b5a4070c 100644
--- a/fdm-app/app/components/blocks/atlas/atlas-legend.tsx
+++ b/fdm-app/app/components/blocks/atlas/atlas-legend.tsx
@@ -5,35 +5,44 @@ interface ElevationLegendProps {
min?: number
max?: number
loading?: boolean
+ hoverValue?: number | null
+ showScale?: boolean
}
-export function ElevationLegend({ min, max, loading }: ElevationLegendProps) {
+export function ElevationLegend({ min, max, loading, hoverValue, showScale = true }: ElevationLegendProps) {
return (
-
+
- Hoogte (NAP)
+ Hoogte (AHN4)
{loading && }
-
-
-
Red)
- background: "linear-gradient(to right, #5e4fa2, #3288bd, #66c2a5, #abdda4, #e6f598, #ffffbf, #fee08b, #fdae61, #f46d43, #d53e4f, #9e0142)"
- }}
- />
+ {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
+
+ )}
-
- {min !== undefined ? `${min.toFixed(1)}m` : "Laag"}
- {max !== undefined ? `${max.toFixed(1)}m` : "Hoog"}
-
-
+ )}
diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
index 39c2a9cc8..8c3e58a56 100644
--- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
+++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
@@ -16,6 +16,7 @@ import {
type MapRef,
type ViewState,
type ViewStateChangeEvent,
+ type MapLayerMouseEvent
} from "react-map-gl/maplibre"
import {
data,
@@ -75,6 +76,7 @@ interface ActiveTile {
id: string
url: string
cogUrl: string | null
+ cogUrlHillshade: string | null
}
// Meta
@@ -146,10 +148,10 @@ export default function FarmAtlasElevationBlock() {
// State
const [indexData, setIndexData] = useState
(null)
const [activeTiles, setActiveTiles] = useState([])
- const [isLoadingCog, setIsLoadingCog] = useState(true)
const [isUpdating, setIsUpdating] = useState(false)
const [legendMin, setLegendMin] = useState(-5)
const [legendMax, setLegendMax] = useState(50)
+ const [hoverElevation, setHoverElevation] = useState(null)
// ViewState logic
const initialViewState = getViewState(fields)
@@ -208,9 +210,19 @@ export default function FarmAtlasElevationBlock() {
const updateVisibleTiles = useCallback(async () => {
if (!mapRef.current || !indexData) return
- setIsUpdating(true)
-
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([])
+ setIsLoadingCog(false)
+ }
+ return
+ }
+
+ setIsUpdating(true)
const sw = bounds.getSouthWest()
const ne = bounds.getNorthEast()
const nw = bounds.getNorthWest()
@@ -227,12 +239,12 @@ export default function FarmAtlasElevationBlock() {
] as [number, number][]
// Find intersecting tiles
- // Optimization: limit to e.g. 6 tiles to avoid overload
+ // 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, 6)
+ }).slice(0, 24)
// Calculate global min/max for the viewport by sampling
const samplePoints: {lng: number, lat: number}[] = []
@@ -257,14 +269,14 @@ export default function FarmAtlasElevationBlock() {
const ring = (f.geometry as any).coordinates[0]
return isPointInPolygon(rdP, ring)
})
- if (feature && feature.properties) {
+ if (feature?.properties) {
const url = feature.properties.url || feature.properties.href || feature.properties.download_url
if (url) {
try {
// Requesting location value
// Note: locationValues caches internal resources so it's efficient
const vals = await locationValues(url, { longitude: p.lng, latitude: p.lat })
- if (vals && vals.length > 0 && !isNaN(vals[0]) && vals[0] > -100 && vals[0] < 1000) {
+ if (vals && vals.length > 0 && !Number.isNaN(vals[0]) && vals[0] > -100 && vals[0] < 1000) {
return vals[0]
}
} catch {}
@@ -298,7 +310,8 @@ export default function FarmAtlasElevationBlock() {
setLegendMax(max)
// Format for color scale
- const colorParam = `#color:BrewerSpectral11,${min},${max},-`
+ // Reverted to BrewerSpectral11 (Reversed, Continuous)
+ const colorParam = `#color:BrewerSpectral11,${min},${max},-c`
const newTiles: ActiveTile[] = []
for (const feature of visibleFeatures) {
@@ -311,10 +324,19 @@ export default function FarmAtlasElevationBlock() {
if (!url) continue
const id = feature.properties.kaartbladNr || url
- newTiles.push({ id, url, cogUrl: `cog://${url}${colorParam}` })
+ newTiles.push({
+ id,
+ url,
+ cogUrl: `cog://${url}${colorParam}`,
+ cogUrlHillshade: `cog://${url}#dem`
+ })
}
// Update state
+ // Simple diff to see if we need to update (comparing URLs including color params)
+ // If colors change, we want to update all tiles
+ const keyNew = newTiles.map(t => t.cogUrl).sort().join("|")
+
setActiveTiles(newTiles)
setIsLoadingCog(false)
@@ -327,16 +349,12 @@ export default function FarmAtlasElevationBlock() {
}, [indexData, activeTiles])
// Throttle updates
- // const throttledUpdate = useMemo(() => throttle(updateVisibleTiles, 500), [updateVisibleTiles])
- // Using throttle directly in render is tricky with deps.
- // We will use a ref to store the latest updateVisibleTiles and throttle that.
-
const updateRef = useRef(updateVisibleTiles)
useEffect(() => { updateRef.current = updateVisibleTiles }, [updateVisibleTiles])
const throttledUpdate = useMemo(() => throttle(() => updateRef.current(), 500, { leading: true, trailing: true }), [])
- // Initial update
+ // Initial update when map loads or index loads
useEffect(() => {
const timer = setTimeout(() => {
throttledUpdate()
@@ -344,16 +362,52 @@ export default function FarmAtlasElevationBlock() {
return () => clearTimeout(timer)
}, [indexData])
+ // Handle hover to show elevation value
+ const handleMouseMove = useCallback(throttle(async (event: MapLayerMouseEvent) => {
+ // If zoomed out (WMS visible), don't fetch values
+ 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]
+
+ // Find tile under mouse
+ // We check activeTiles first as they are already filtered
+ // But we need geometry. indexData has geometry.
+ // Find matching feature in indexData
+ const feature = indexData.features.find((f) => {
+ // Optimization: check if ID matches an active tile?
+ 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) {
+ // Use locationValues
+ 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) {
+ // console.warn("Error fetching hover elevation", e)
+ setHoverElevation(null)
+ }
+ }, 100), [indexData, activeTiles])
+
return (
-
- {isLoadingCog && (
-
-
-
- Hoogtekaart laden...
-
-
- )}
+
@@ -377,32 +432,75 @@ export default function FarmAtlasElevationBlock() {
}
/>
- {/* Render Active Tiles */}
- {activeTiles.map((tile) => (
+ {/* WMS Overview Layer (Zoom < 13) */}
+ {viewState.zoom < 13 && (
+ )}
+
+ {/* Render Active Tiles (Zoom >= 13) */}
+ {activeTiles.map((tile) => (
+ <>
+
+
+
+
+
+
+ >
))}
= 13}
/>
)
-}
+}
\ No newline at end of file
From 4bc694b422a82328df91558cfa0fd11e74aa556d Mon Sep 17 00:00:00 2001
From: Sven Verweij <37927107+SvenVw@users.noreply.github.com>
Date: Fri, 5 Dec 2025 15:21:48 +0100
Subject: [PATCH 03/13] feat: make elevation layer available in app
---
.../blocks/atlas/atlas-controls.tsx | 84 +++++++++++++---
.../components/blocks/atlas/atlas-legend.tsx | 2 +-
.../app/components/blocks/header/atlas.tsx | 36 ++++++-
.../app/components/blocks/sidebar/apps.tsx | 90 ++++++++++++-----
...m.$b_id_farm.$calendar.atlas.elevation.tsx | 99 +++++++++++--------
fdm-app/package.json | 1 +
6 files changed, 229 insertions(+), 83 deletions(-)
diff --git a/fdm-app/app/components/blocks/atlas/atlas-controls.tsx b/fdm-app/app/components/blocks/atlas/atlas-controls.tsx
index e3400b754..a68ee5ca1 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
}
@@ -104,14 +115,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 +136,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
index 1b5a4070c..4748c1c4c 100644
--- a/fdm-app/app/components/blocks/atlas/atlas-legend.tsx
+++ b/fdm-app/app/components/blocks/atlas/atlas-legend.tsx
@@ -11,7 +11,7 @@ interface ElevationLegendProps {
export function ElevationLegend({ min, max, loading, hoverValue, showScale = true }: ElevationLegendProps) {
return (
-
+
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}
+
+
+
+
+
(-5)
const [legendMax, setLegendMax] = useState(50)
const [hoverElevation, setHoverElevation] = useState(null)
+ const [showFields, setShowFields] = useState(true)
+ const [showElevation, setShowElevation] = useState(true)
+
+ const fieldsSavedId = "fieldsSaved"
+ const fieldsSavedStyle = getFieldsStyle(fieldsSavedId)
+ 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(
- "mapViewStateElevation",
+ "mapViewState",
)
if (savedViewState) {
try {
return JSON.parse(savedViewState)
} catch {
- sessionStorage.removeItem("mapViewStateElevation")
+ sessionStorage.removeItem("mapViewState")
}
}
}
@@ -183,7 +196,7 @@ export default function FarmAtlasElevationBlock() {
return
}
sessionStorage.setItem(
- "mapViewStateElevation",
+ "mapViewState",
JSON.stringify(viewState),
)
}, [viewState])
@@ -200,7 +213,6 @@ export default function FarmAtlasElevationBlock() {
setIndexData(data)
} catch (e) {
console.error("Error fetching COG index:", e)
- setIsLoadingCog(false)
}
}
fetchIndex()
@@ -209,7 +221,7 @@ export default function FarmAtlasElevationBlock() {
// Function to update visible tiles
const updateVisibleTiles = useCallback(async () => {
if (!mapRef.current || !indexData) return
-
+
const bounds = mapRef.current.getBounds()
const zoom = mapRef.current.getZoom()
@@ -217,7 +229,6 @@ export default function FarmAtlasElevationBlock() {
if (zoom < 13) {
if (activeTiles.length > 0) {
setActiveTiles([])
- setIsLoadingCog(false)
}
return
}
@@ -269,14 +280,13 @@ export default function FarmAtlasElevationBlock() {
const ring = (f.geometry as any).coordinates[0]
return isPointInPolygon(rdP, ring)
})
- if (feature?.properties) {
+ if (feature && feature.properties) {
const url = feature.properties.url || feature.properties.href || feature.properties.download_url
if (url) {
try {
// Requesting location value
- // Note: locationValues caches internal resources so it's efficient
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) {
+ if (vals && vals.length > 0 && !isNaN(vals[0]) && vals[0] > -100 && vals[0] < 1000) {
return vals[0]
}
} catch {}
@@ -290,7 +300,6 @@ export default function FarmAtlasElevationBlock() {
min = Math.min(...validValues)
max = Math.max(...validValues)
} else {
- // Fallback if no data found (e.g. outside coverage or error)
min = -5
max = 50
}
@@ -310,7 +319,6 @@ export default function FarmAtlasElevationBlock() {
setLegendMax(max)
// Format for color scale
- // Reverted to BrewerSpectral11 (Reversed, Continuous)
const colorParam = `#color:BrewerSpectral11,${min},${max},-c`
const newTiles: ActiveTile[] = []
@@ -332,13 +340,9 @@ export default function FarmAtlasElevationBlock() {
})
}
- // Update state
- // Simple diff to see if we need to update (comparing URLs including color params)
- // If colors change, we want to update all tiles
const keyNew = newTiles.map(t => t.cogUrl).sort().join("|")
setActiveTiles(newTiles)
- setIsLoadingCog(false)
} catch (e) {
console.error("Error updating visible tiles:", e)
@@ -360,11 +364,10 @@ export default function FarmAtlasElevationBlock() {
throttledUpdate()
}, 1000)
return () => clearTimeout(timer)
- }, [indexData])
+ }, [throttledUpdate])
// Handle hover to show elevation value
const handleMouseMove = useCallback(throttle(async (event: MapLayerMouseEvent) => {
- // If zoomed out (WMS visible), don't fetch values
if (!mapRef.current || mapRef.current.getZoom() < 13) {
setHoverElevation(null)
return
@@ -377,23 +380,17 @@ export default function FarmAtlasElevationBlock() {
try {
const rdP = proj4("EPSG:28992").forward([lng, lat]) as [number, number]
- // Find tile under mouse
- // We check activeTiles first as they are already filtered
- // But we need geometry. indexData has geometry.
- // Find matching feature in indexData
const feature = indexData.features.find((f) => {
- // Optimization: check if ID matches an active tile?
if (!f.geometry || f.geometry.type !== "Polygon") return false
const ring = (f.geometry as any).coordinates[0]
return isPointInPolygon(rdP, ring)
})
- if (feature?.properties) {
+ if (feature && feature.properties) {
const url = feature.properties.url || feature.properties.href || feature.properties.download_url
if (url) {
- // Use locationValues
const values = await locationValues(url, { longitude: lng, latitude: lat })
- if (values && values.length > 0 && !Number.isNaN(values[0])) {
+ if (values && values.length > 0 && !isNaN(values[0])) {
setHoverElevation(values[0])
return
}
@@ -401,13 +398,12 @@ export default function FarmAtlasElevationBlock() {
}
setHoverElevation(null)
} catch (e) {
- // console.warn("Error fetching hover elevation", e)
setHoverElevation(null)
}
- }, 100), [indexData, activeTiles])
+ }, 100), [])
return (
-
+
@@ -430,10 +426,14 @@ export default function FarmAtlasElevationBlock() {
zoom,
}))
}
+ showFields={showFields}
+ onToggleFields={() => setShowFields(!showFields)}
+ showElevation={showElevation}
+ onToggleElevation={onToggleElevation}
/>
{/* WMS Overview Layer (Zoom < 13) */}
- {viewState.zoom < 13 && (
+ {viewState.zoom < 13 && showElevation && (
= 13) */}
- {activeTiles.map((tile) => (
+ {viewState.zoom >= 13 && showElevation && activeTiles.map((tile) => (
<>
))}
- = 13}
- />
+ {/* Fields Overlay (Saved Fields) */}
+ {fields && (
+
+
+
+ )}
+
+
+
= 13 && showElevation}
+ />
+
+
+
+
)
diff --git a/fdm-app/package.json b/fdm-app/package.json
index c81043483..561524ed6 100644
--- a/fdm-app/package.json
+++ b/fdm-app/package.json
@@ -41,6 +41,7 @@
"@tailwindcss/vite": "^4.1.17",
"@tanstack/react-table": "^8.21.3",
"@turf/centroid": "^7.3.1",
+ "@turf/boolean-intersects": "^7.3.1",
"@turf/turf": "^7.3.1",
"better-auth": "catalog:",
"chrono-node": "^2.9.0",
From 4d2d6188c727534e5bded306f9c81a11d42bb2a2 Mon Sep 17 00:00:00 2001
From: Sven Verweij <37927107+SvenVw@users.noreply.github.com>
Date: Fri, 5 Dec 2025 16:23:09 +0100
Subject: [PATCH 04/13] feat: add attribution of AHN
---
.../routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
index 83670179b..ee3c73c53 100644
--- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
+++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
@@ -280,7 +280,7 @@ export default function FarmAtlasElevationBlock() {
const ring = (f.geometry as any).coordinates[0]
return isPointInPolygon(rdP, ring)
})
- if (feature && feature.properties) {
+ if (feature?.properties) {
const url = feature.properties.url || feature.properties.href || feature.properties.download_url
if (url) {
try {
@@ -386,7 +386,7 @@ export default function FarmAtlasElevationBlock() {
return isPointInPolygon(rdP, ring)
})
- if (feature && feature.properties) {
+ 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 })
@@ -441,6 +441,7 @@ export default function FarmAtlasElevationBlock() {
"https://service.pdok.nl/rws/ahn/wms/v1_0?service=WMS&request=GetMap&layers=dtm_05m&styles=&format=image/png&transparent=true&version=1.3.0&width=256&height=256&crs=EPSG:3857&bbox={bbox-epsg-3857}"
]}
tileSize={256}
+ attribution="©
PDOK,
AHN"
>
Date: Mon, 8 Dec 2025 10:18:57 +0100
Subject: [PATCH 05/13] fix: errors in console
---
.../blocks/atlas/atlas-controls.tsx | 5 +-
...m.$b_id_farm.$calendar.atlas.elevation.tsx | 56 +++++++++++--------
2 files changed, 36 insertions(+), 25 deletions(-)
diff --git a/fdm-app/app/components/blocks/atlas/atlas-controls.tsx b/fdm-app/app/components/blocks/atlas/atlas-controls.tsx
index a68ee5ca1..3ab8e23cb 100644
--- a/fdm-app/app/components/blocks/atlas/atlas-controls.tsx
+++ b/fdm-app/app/components/blocks/atlas/atlas-controls.tsx
@@ -103,7 +103,10 @@ class CustomControl 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)
diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
index ee3c73c53..a43ceba1f 100644
--- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
+++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
@@ -8,7 +8,7 @@ import { getFields } from "@svenvw/fdm-core"
import throttle from "lodash.throttle"
import type { FeatureCollection } from "geojson"
import maplibregl from "maplibre-gl"
-import { useCallback, useEffect, useMemo, useRef, useState } from "react"
+import { useCallback, useEffect, useMemo, useRef, useState, Fragment } from "react"
import {
Layer,
Map as MapGL,
@@ -273,24 +273,26 @@ export default function FarmAtlasElevationBlock() {
// Gather values for samples
const values = await Promise.all(samplePoints.map(async (p) => {
- 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) {
- try {
- // Requesting location value
- const vals = await locationValues(url, { longitude: p.lng, latitude: p.lat })
- if (vals && vals.length > 0 && !isNaN(vals[0]) && vals[0] > -100 && vals[0] < 1000) {
- return vals[0]
- }
- } catch {}
- }
+ 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 && !isNaN(vals[0]) && vals[0] > -100 && vals[0] < 1000) {
+ return vals[0]
+ }
+ }
+ }
+ } catch {
+ // Ignore errors for individual points
}
return null
}))
@@ -366,8 +368,16 @@ export default function FarmAtlasElevationBlock() {
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 = useCallback(throttle(async (event: MapLayerMouseEvent) => {
+ const handleMouseMove = useMemo(() => throttle(async (event: MapLayerMouseEvent) => {
+ const { indexData, activeTiles } = stateRef.current
+
if (!mapRef.current || mapRef.current.getZoom() < 13) {
setHoverElevation(null)
return
@@ -453,9 +463,8 @@ export default function FarmAtlasElevationBlock() {
{/* Render Active Tiles (Zoom >= 13) */}
{viewState.zoom >= 13 && showElevation && activeTiles.map((tile) => (
- <>
+
- >
+
))}
{/* Fields Overlay (Saved Fields) */}
From d0c88528ad05f2c1cc01b7dec89e0f036f0858a9 Mon Sep 17 00:00:00 2001
From: Sven Verweij <37927107+SvenVw@users.noreply.github.com>
Date: Tue, 9 Dec 2025 09:33:19 +0100
Subject: [PATCH 06/13] fix: show saved fields on top of elevation layer
---
...m.$b_id_farm.$calendar.atlas.elevation.tsx | 350 +++++++++++-------
1 file changed, 208 insertions(+), 142 deletions(-)
diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
index a43ceba1f..df86d6808 100644
--- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
+++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
@@ -8,7 +8,14 @@ import { getFields } from "@svenvw/fdm-core"
import throttle from "lodash.throttle"
import type { FeatureCollection } from "geojson"
import maplibregl from "maplibre-gl"
-import { useCallback, useEffect, useMemo, useRef, useState, Fragment } from "react"
+import {
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+ Fragment,
+} from "react"
import {
Layer,
Map as MapGL,
@@ -16,7 +23,7 @@ import {
type MapRef,
type ViewState,
type ViewStateChangeEvent,
- type MapLayerMouseEvent
+ type MapLayerMouseEvent,
} from "react-map-gl/maplibre"
import {
data,
@@ -67,7 +74,10 @@ function isPointInPolygon(point: [number, number], vs: [number, number][]) {
// Helper: Check if polygon intersects polygon (simple AABB check for index speed, then detail?)
// 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][]) {
+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
@@ -160,6 +170,7 @@ export default function FarmAtlasElevationBlock() {
const fieldsSavedId = "fieldsSaved"
const fieldsSavedStyle = getFieldsStyle(fieldsSavedId)
+ const fieldsSavedOutlineStyle = getFieldsStyle("fieldsSavedOutline")
const layerLayout = { visibility: showFields ? "visible" : "none" } as const
const onToggleElevation = useCallback(() => {
@@ -170,9 +181,7 @@ export default function FarmAtlasElevationBlock() {
const initialViewState = getViewState(fields)
const [viewState, setViewState] = useState(() => {
if (typeof window !== "undefined") {
- const savedViewState = sessionStorage.getItem(
- "mapViewState",
- )
+ const savedViewState = sessionStorage.getItem("mapViewState")
if (savedViewState) {
try {
return JSON.parse(savedViewState)
@@ -195,10 +204,7 @@ export default function FarmAtlasElevationBlock() {
isFirstRender.current = false
return
}
- sessionStorage.setItem(
- "mapViewState",
- JSON.stringify(viewState),
- )
+ sessionStorage.setItem("mapViewState", JSON.stringify(viewState))
}, [viewState])
// Fetch COG Index once
@@ -251,14 +257,17 @@ export default function FarmAtlasElevationBlock() {
// 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)
+ 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 samplePoints: { lng: number; lat: number }[] = []
const gridSize = 4 // 4x4 = 16 points
for (let i = 0; i <= gridSize; i++) {
for (let j = 0; j <= gridSize; j++) {
@@ -270,34 +279,52 @@ export default function FarmAtlasElevationBlock() {
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 && !isNaN(vals[0]) && vals[0] > -100 && vals[0] < 1000) {
- return vals[0]
+ 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 &&
+ !isNaN(vals[0]) &&
+ vals[0] > -100 &&
+ vals[0] < 1000
+ ) {
+ return vals[0]
+ }
}
}
+ } catch {
+ // Ignore errors for individual points
}
- } catch {
- // Ignore errors for individual points
- }
- return null
- }))
+ return null
+ }),
+ )
- const validValues = values.filter(v => v !== null) as number[]
+ const validValues = values.filter((v) => v !== null) as number[]
if (validValues.length > 0) {
min = Math.min(...validValues)
max = Math.max(...validValues)
@@ -311,12 +338,12 @@ export default function FarmAtlasElevationBlock() {
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)
@@ -330,35 +357,45 @@ export default function FarmAtlasElevationBlock() {
feature.properties.url ||
feature.properties.href ||
feature.properties.download_url
-
+
if (!url) continue
const id = feature.properties.kaartbladNr || url
-
- newTiles.push({
- id,
- url,
+
+ newTiles.push({
+ id,
+ url,
cogUrl: `cog://${url}${colorParam}`,
- cogUrlHillshade: `cog://${url}#dem`
+ cogUrlHillshade: `cog://${url}#dem`,
})
}
- const keyNew = newTiles.map(t => t.cogUrl).sort().join("|")
-
+ const keyNew = newTiles
+ .map((t) => t.cogUrl)
+ .sort()
+ .join("|")
+
setActiveTiles(newTiles)
-
} catch (e) {
console.error("Error updating visible tiles:", e)
} finally {
setIsUpdating(false)
}
-
}, [indexData, activeTiles])
// Throttle updates
const updateRef = useRef(updateVisibleTiles)
- useEffect(() => { updateRef.current = updateVisibleTiles }, [updateVisibleTiles])
-
- const throttledUpdate = useMemo(() => throttle(() => updateRef.current(), 500, { leading: true, trailing: true }), [])
+ 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(() => {
@@ -375,46 +412,63 @@ export default function FarmAtlasElevationBlock() {
}, [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
- }
+ 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 (!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 && !isNaN(values[0])) {
- setHoverElevation(values[0])
- return
+ 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 &&
+ !isNaN(values[0])
+ ) {
+ setHoverElevation(values[0])
+ return
+ }
+ }
}
+ setHoverElevation(null)
+ } catch (e) {
+ setHoverElevation(null)
}
- }
- setHoverElevation(null)
- } catch (e) {
- setHoverElevation(null)
- }
- }, 100), [])
+ }, 100),
+ [],
+ )
return (
-
-
+
-
)}
{/* Render Active Tiles (Zoom >= 13) */}
- {viewState.zoom >= 13 && showElevation && activeTiles.map((tile) => (
-
-
-
-
-
-
-
-
- ))}
+ {viewState.zoom >= 13 &&
+ showElevation &&
+ activeTiles.map((tile) => (
+
+
+
+
+
+
+
+
+ ))}
{/* Fields Overlay (Saved Fields) */}
{fields && (
-
+
+ {/* Outline Layer - Visual */}
+ {/* Fill Layer - Invisible but Clickable/Hoverable */}
+
)}
- = 13 && showElevation}
@@ -534,4 +600,4 @@ export default function FarmAtlasElevationBlock() {
)
-}
\ No newline at end of file
+}
From 36c04f019126d8a6411ae0ed5eab82d2c536baf7 Mon Sep 17 00:00:00 2001
From: Sven Verweij <37927107+SvenVw@users.noreply.github.com>
Date: Tue, 9 Dec 2025 09:43:50 +0100
Subject: [PATCH 07/13] feat: add message if connection is slow
---
.../components/blocks/atlas/atlas-legend.tsx | 15 +++-
...m.$b_id_farm.$calendar.atlas.elevation.tsx | 70 ++++++++++++++++---
2 files changed, 75 insertions(+), 10 deletions(-)
diff --git a/fdm-app/app/components/blocks/atlas/atlas-legend.tsx b/fdm-app/app/components/blocks/atlas/atlas-legend.tsx
index 4748c1c4c..6aa9f17ff 100644
--- a/fdm-app/app/components/blocks/atlas/atlas-legend.tsx
+++ b/fdm-app/app/components/blocks/atlas/atlas-legend.tsx
@@ -7,9 +7,10 @@ interface ElevationLegendProps {
loading?: boolean
hoverValue?: number | null
showScale?: boolean
+ networkStatus?: "idle" | "loading" | "slow" | "error"
}
-export function ElevationLegend({ min, max, loading, hoverValue, showScale = true }: ElevationLegendProps) {
+export function ElevationLegend({ min, max, loading, hoverValue, showScale = true, networkStatus }: ElevationLegendProps) {
return (
@@ -21,6 +22,18 @@ export function ElevationLegend({ min, max, loading, hoverValue, showScale = tru
{loading && }
+ {networkStatus === "slow" && (
+
+ Trage verbinding...
+
+ )}
+
+ {networkStatus === "error" && (
+
+ Fout bij laden
+
+ )}
+
{showScale && (
diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
index df86d6808..a18a51f79 100644
--- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
+++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
@@ -167,6 +167,9 @@ export default function FarmAtlasElevationBlock() {
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)
@@ -210,13 +213,45 @@ export default function FarmAtlasElevationBlock() {
// Fetch COG Index once
useEffect(() => {
async function fetchIndex() {
+ const cacheKey = "ahn_kaartbladindex_v1"
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)
+ 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)
+
+ 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)
}
@@ -224,6 +259,8 @@ export default function FarmAtlasElevationBlock() {
fetchIndex()
}, [])
+ const updateId = useRef(0)
+
// Function to update visible tiles
const updateVisibleTiles = useCallback(async () => {
if (!mapRef.current || !indexData) return
@@ -239,7 +276,17 @@ export default function FarmAtlasElevationBlock() {
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()
@@ -268,7 +315,7 @@ export default function FarmAtlasElevationBlock() {
// Calculate global min/max for the viewport by sampling
const samplePoints: { lng: number; lat: number }[] = []
- const gridSize = 4 // 4x4 = 16 points
+ 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)
@@ -309,7 +356,7 @@ export default function FarmAtlasElevationBlock() {
if (
vals &&
vals.length > 0 &&
- !isNaN(vals[0]) &&
+ !Number.isNaN(vals[0]) &&
vals[0] > -100 &&
vals[0] < 1000
) {
@@ -324,6 +371,8 @@ export default function FarmAtlasElevationBlock() {
}),
)
+ if (updateId.current !== currentId) return
+
const validValues = values.filter((v) => v !== null) as number[]
if (validValues.length > 0) {
min = Math.min(...validValues)
@@ -369,16 +418,18 @@ export default function FarmAtlasElevationBlock() {
})
}
- const keyNew = newTiles
- .map((t) => t.cogUrl)
- .sort()
- .join("|")
-
setActiveTiles(newTiles)
+ setNetworkStatus("idle")
} catch (e) {
console.error("Error updating visible tiles:", e)
+ if (updateId.current === currentId) {
+ setNetworkStatus("error")
+ }
} finally {
- setIsUpdating(false)
+ if (updateId.current === currentId) {
+ setIsUpdating(false)
+ }
+ clearTimeout(slowTimer)
}
}, [indexData, activeTiles])
@@ -463,7 +514,7 @@ export default function FarmAtlasElevationBlock() {
} catch (e) {
setHoverElevation(null)
}
- }, 100),
+ }, 200),
[],
)
@@ -589,6 +640,7 @@ export default function FarmAtlasElevationBlock() {
loading={isUpdating}
hoverValue={hoverElevation}
showScale={viewState.zoom >= 13 && showElevation}
+ networkStatus={networkStatus}
/>
Date: Mon, 15 Dec 2025 10:47:52 +0100
Subject: [PATCH 08/13] fix: installing maplibre-cog-protocol from fork
---
fdm-app/package.json | 3 ++-
pnpm-lock.yaml | 11 ++++++-----
2 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/fdm-app/package.json b/fdm-app/package.json
index 256a55206..b531239a9 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,8 +41,8 @@
"@svenvw/fdm-data": "workspace:*",
"@tailwindcss/vite": "^4.1.17",
"@tanstack/react-table": "^8.21.3",
- "@turf/centroid": "^7.3.1",
"@turf/boolean-intersects": "^7.3.1",
+ "@turf/centroid": "^7.3.1",
"@turf/turf": "^7.3.1",
"better-auth": "catalog:",
"chrono-node": "^2.9.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 164fa6333..83a2197f0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -80,8 +80,8 @@ importers:
specifier: ^1.4.1
version: 1.4.1
'@geomatico/maplibre-cog-protocol':
- specifier: file:C:/Users/sven.verweij/Applications/packages/maplibre-cog-protocol
- version: file:../packages/maplibre-cog-protocol(maplibre-gl@5.13.0)
+ specifier: github:SvenVw/maplibre-cog-protocol#add-prepare
+ version: https://codeload.github.com/SvenVw/maplibre-cog-protocol/tar.gz/fdb1f2505071f6bd3dc94fe07024d3ddfe826904(maplibre-gl@5.13.0)
'@hookform/resolvers':
specifier: ^5.2.2
version: 5.2.2(react-hook-form@7.68.0(react@19.2.1))
@@ -2230,8 +2230,9 @@ packages:
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
- '@geomatico/maplibre-cog-protocol@file:../packages/maplibre-cog-protocol':
- resolution: {directory: ../packages/maplibre-cog-protocol, type: directory}
+ '@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
@@ -12977,7 +12978,7 @@ snapshots:
'@floating-ui/utils@0.2.10': {}
- '@geomatico/maplibre-cog-protocol@file:../packages/maplibre-cog-protocol(maplibre-gl@5.13.0)':
+ '@geomatico/maplibre-cog-protocol@https://codeload.github.com/SvenVw/maplibre-cog-protocol/tar.gz/fdb1f2505071f6bd3dc94fe07024d3ddfe826904(maplibre-gl@5.13.0)':
dependencies:
'@mapbox/sphericalmercator': 1.2.0
d3-scale: 4.0.2
From 39a5177f2f888d5d120f99a909c11cf1268ccdf2 Mon Sep 17 00:00:00 2001
From: Sven Verweij <37927107+SvenVw@users.noreply.github.com>
Date: Mon, 15 Dec 2025 11:04:49 +0100
Subject: [PATCH 09/13] fix: show elevation layer when no farm is selected
---
...m.$b_id_farm.$calendar.atlas.elevation.tsx | 70 +++++++++++--------
1 file changed, 40 insertions(+), 30 deletions(-)
diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
index a18a51f79..daf01d80e 100644
--- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
+++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
@@ -1,6 +1,5 @@
import {
cogProtocol,
- getCogMetadata,
locationValues,
proj4,
} from "@geomatico/maplibre-cog-protocol"
@@ -26,7 +25,6 @@ import {
type MapLayerMouseEvent,
} from "react-map-gl/maplibre"
import {
- data,
type LoaderFunctionArgs,
type MetaFunction,
useLoaderData,
@@ -37,7 +35,6 @@ import { FieldsPanelHover } from "~/components/blocks/atlas/atlas-panels"
import { getFieldsStyle } from "~/components/blocks/atlas/atlas-styles"
import { ZOOM_LEVEL_FIELDS } from "~/components/blocks/atlas/atlas"
import { getViewState } from "~/components/blocks/atlas/atlas-viewstate"
-import { LoadingSpinner } from "~/components/custom/loadingspinner"
import { getMapStyle } from "~/integrations/map"
import { getSession } from "~/lib/auth.server"
import { getCalendar, getTimeframe } from "~/lib/calendar"
@@ -109,34 +106,39 @@ export const meta: MetaFunction = () => {
export async function loader({ request, params }: LoaderFunctionArgs) {
try {
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",
- })
- }
const session = await getSession(request)
const calendar = getCalendar(params)
const timeframe = getTimeframe(params)
- const fields = await getFields(
- fdm,
- session.principal_id,
- b_id_farm,
- timeframe,
- )
- const featureCollection: FeatureCollection = {
- type: "FeatureCollection",
- features: fields.map((field) => ({
- 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,
- })),
+ // Get the fields of the farm
+ 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
+ })
+
+ featureCollection = {
+ type: "FeatureCollection",
+ features: features,
+ }
}
const mapStyle = getMapStyle("satellite")
@@ -503,7 +505,7 @@ export default function FarmAtlasElevationBlock() {
if (
values &&
values.length > 0 &&
- !isNaN(values[0])
+ !Number.isNaN(values[0])
) {
setHoverElevation(values[0])
return
@@ -562,7 +564,7 @@ export default function FarmAtlasElevationBlock() {
id="ahn-wms-layer"
type="raster"
paint={{ "raster-opacity": 0.8 }}
- beforeId="fieldsSavedOutline"
+ beforeId={fields ? "fieldsSavedOutline" : undefined}
/>
)}
@@ -586,7 +588,11 @@ export default function FarmAtlasElevationBlock() {
id={`ahn-layer-${tile.id}`}
type="raster"
paint={{ "raster-opacity": 1 }}
- beforeId="fieldsSavedOutline"
+ beforeId={
+ fields
+ ? "fieldsSavedOutline"
+ : undefined
+ }
/>
From da6a314b9e308ca4f651e3419267aa6b2eeb8a79 Mon Sep 17 00:00:00 2001
From: Sven Verweij <37927107+SvenVw@users.noreply.github.com>
Date: Mon, 15 Dec 2025 13:20:55 +0100
Subject: [PATCH 10/13] fix: broken lockfile
---
pnpm-lock.yaml | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 84c1bff26..617eea9cd 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -81,7 +81,7 @@ importers:
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.13.0)
+ 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))
@@ -11414,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
@@ -11425,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
@@ -12914,12 +12914,12 @@ 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.13.0)':
+ '@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.13.0
+ maplibre-gl: 5.14.0
proj4: 2.20.2
quick-lru: 7.3.0
@@ -17031,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
From 3093c59ddf21e26467f13a1ea03bae5ba3a55ca0 Mon Sep 17 00:00:00 2001
From: Sven Verweij <37927107+SvenVw@users.noreply.github.com>
Date: Wed, 17 Dec 2025 09:55:37 +0100
Subject: [PATCH 11/13] refactor: use Atlas for page title
---
.../routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx | 6 +++---
.../farm.$b_id_farm.$calendar.atlas.fields._index.tsx | 2 +-
fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.tsx | 2 +-
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
index daf01d80e..534464a05 100644
--- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
+++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
@@ -68,8 +68,8 @@ function isPointInPolygon(point: [number, number], vs: [number, number][]) {
return inside
}
-// Helper: Check if polygon intersects polygon (simple AABB check for index speed, then detail?)
-// For now, we just check if any point of tile is in view or view in tile?
+// 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][],
@@ -92,7 +92,7 @@ interface ActiveTile {
// Meta
export const meta: MetaFunction = () => {
return [
- { title: `Hoogte - Kaart | ${clientConfig.name}` },
+ { title: `Hoogte - Atlas | ${clientConfig.name}` },
{
name: "description",
content: "Bekijk hoogtegegevens op de kaart.",
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.",
From ba8ef5ff2e9d3a3d1a5584a73c3b7b047faf25f8 Mon Sep 17 00:00:00 2001
From: Sven Verweij <37927107+SvenVw@users.noreply.github.com>
Date: Wed, 17 Dec 2025 10:12:07 +0100
Subject: [PATCH 12/13] refactor: improve transition between WMS and COG for
elevation map
---
fdm-app/app/components/blocks/atlas/atlas-legend.tsx | 9 ++++++++-
.../farm.$b_id_farm.$calendar.atlas.elevation.tsx | 10 +++++++---
2 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/fdm-app/app/components/blocks/atlas/atlas-legend.tsx b/fdm-app/app/components/blocks/atlas/atlas-legend.tsx
index 6aa9f17ff..d67af11b7 100644
--- a/fdm-app/app/components/blocks/atlas/atlas-legend.tsx
+++ b/fdm-app/app/components/blocks/atlas/atlas-legend.tsx
@@ -8,9 +8,10 @@ interface ElevationLegendProps {
hoverValue?: number | null
showScale?: boolean
networkStatus?: "idle" | "loading" | "slow" | "error"
+ message?: string
}
-export function ElevationLegend({ min, max, loading, hoverValue, showScale = true, networkStatus }: ElevationLegendProps) {
+export function ElevationLegend({ min, max, loading, hoverValue, showScale = true, networkStatus, message }: ElevationLegendProps) {
return (
@@ -33,6 +34,12 @@ export function ElevationLegend({ min, max, loading, hoverValue, showScale = tru
Fout bij laden
)}
+
+ {message && (
+
+ {message}
+
+ )}
{showScale && (
diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
index 534464a05..8dd43d602 100644
--- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
+++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
@@ -550,7 +550,7 @@ export default function FarmAtlasElevationBlock() {
/>
{/* WMS Overview Layer (Zoom < 13) */}
- {viewState.zoom < 13 && showElevation && (
+ {showElevation && (
= 13) */}
- {viewState.zoom >= 13 &&
- showElevation &&
+ {showElevation &&
activeTiles.map((tile) => (
= 13 && showElevation}
networkStatus={networkStatus}
+ message={
+ showElevation && viewState.zoom < 13
+ ? "Zoom in voor meer detail"
+ : undefined
+ }
/>
Date: Wed, 17 Dec 2025 10:29:49 +0100
Subject: [PATCH 13/13] refactor: Add user-facing feedback for index fetch
failures
---
.../app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
index 8dd43d602..0aed0eddb 100644
--- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
+++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
@@ -216,6 +216,7 @@ export default function FarmAtlasElevationBlock() {
useEffect(() => {
async function fetchIndex() {
const cacheKey = "ahn_kaartbladindex_v1"
+ setNetworkStatus("loading")
try {
// Try cache
if (typeof localStorage !== "undefined") {
@@ -229,6 +230,7 @@ export default function FarmAtlasElevationBlock() {
7 * 24 * 60 * 60 * 1000
) {
setIndexData(data)
+ setNetworkStatus("idle")
return
}
} catch {
@@ -243,6 +245,7 @@ export default function FarmAtlasElevationBlock() {
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 {
@@ -256,6 +259,7 @@ export default function FarmAtlasElevationBlock() {
}
} catch (e) {
console.error("Error fetching COG index:", e)
+ setNetworkStatus("error")
}
}
fetchIndex()