Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/jolly-ravens-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@svenvw/fdm-app": minor
---

Add implementation of the AHN4 via the elevation layer in Atlas
89 changes: 73 additions & 16 deletions fdm-app/app/components/blocks/atlas/atlas-controls.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -19,6 +19,8 @@ type ControlsProps = {
}) => void
showFields?: boolean
onToggleFields?: () => void
showElevation?: boolean
onToggleElevation?: () => void
}

export function Controls(props: ControlsProps) {
Expand All @@ -36,6 +38,12 @@ export function Controls(props: ControlsProps) {
onToggle={props.onToggleFields}
/>
)}
{props.showElevation !== undefined && props.onToggleElevation && (
<ElevationControl
showElevation={props.showElevation}
onToggle={props.onToggleElevation}
/>
)}
<GeolocateControl
positionOptions={{ enableHighAccuracy: true }}
trackUserLocation={true}
Expand All @@ -45,12 +53,15 @@ export function Controls(props: ControlsProps) {
)
}

interface FieldsButtonProps {
showFields: boolean
interface ButtonProps {
active: boolean
onToggle: () => void
labelActive: string
labelInactive: string
Icon: React.ElementType
}

function FieldsButton({ showFields, onToggle }: FieldsButtonProps) {
function ControlButton({ active, onToggle, labelActive, labelInactive, Icon }: ButtonProps) {
return (
<button
type="button"
Expand All @@ -60,22 +71,22 @@ function FieldsButton({ showFields, onToggle }: FieldsButtonProps) {
e.stopPropagation()
onToggle()
}}
title={showFields ? "Verberg percelen" : "Toon percelen"}
title={active ? labelActive : labelInactive}
>
<Layers
className={`h-5 w-full ${showFields ? "opacity-100" : "opacity-40"}`}
<Icon
className={`h-5 w-full ${active ? "opacity-100" : "opacity-40"}`}
/>
</button>
)
}

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
}

Expand All @@ -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)
Expand All @@ -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(<FieldsButton {...this._props} />)
this._root.render(<ControlButton {...this._props} />)
}
}
}
Expand All @@ -125,14 +139,57 @@ function FieldsControl({
showFields: boolean
onToggle: () => void
}) {
const control = useControl<CustomFieldsControl>(
() => new CustomFieldsControl({ showFields, onToggle }),
const control = useControl<CustomControl>(
() => 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<CustomControl>(
() => 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
}
70 changes: 70 additions & 0 deletions fdm-app/app/components/blocks/atlas/atlas-legend.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="w-40">
<Card className="bg-background/90 backdrop-blur-sm shadow-sm">
<CardContent className="p-3">
<div className="flex items-center justify-between mb-2">
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Hoogte (AHN4)
</h4>
{loading && <LoadingSpinner className="h-3 w-3" />}
</div>

{networkStatus === "slow" && (
<div className="mb-2 text-xs font-medium text-orange-600">
Trage verbinding...
</div>
)}

{networkStatus === "error" && (
<div className="mb-2 text-xs font-medium text-destructive">
Fout bij laden
</div>
)}

{message && (
<div className="mb-2 text-xs font-medium text-muted-foreground">
{message}
</div>
)}

{showScale && (
<div className="flex flex-col gap-1">
<div className="flex h-4 w-full rounded border border-border overflow-hidden relative">
<div
className="absolute inset-0 w-full h-full"
style={{
// BrewerSpectral11 Reversed (Blue -> Red)
background: "linear-gradient(to right, #5e4fa2, #3288bd, #66c2a5, #abdda4, #e6f598, #ffffbf, #fee08b, #fdae61, #f46d43, #d53e4f, #9e0142)"
}}
/>
</div>
<div className="flex justify-between text-[12px] text-muted-foreground font-medium font-mono">
<span>{min !== undefined ? `${min.toFixed(1)}m` : "Laag"}</span>
<span>{max !== undefined ? `${max.toFixed(1)}m` : "Hoog"}</span>
</div>
{hoverValue !== undefined && hoverValue !== null && (
<div className="mt-2 text-left text-xs font-bold">
Hoogte: {hoverValue.toFixed(2)} m NAP
</div>
)}
</div>
)}
</CardContent>
</Card>
</div>
)
}
36 changes: 31 additions & 5 deletions fdm-app/app/components/blocks/header/atlas.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
Expand All @@ -18,11 +31,24 @@ export function HeaderAtlas({ b_id_farm }: { b_id_farm: string | undefined }) {
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink
href={`/farm/${b_id_farm}/${calendar}/atlas/fields`}
>
Percelen
</BreadcrumbLink>
<DropdownMenu>
<DropdownMenuTrigger className="flex items-center gap-1">
{currentName}
<ChevronDown className="h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem asChild>
<NavLink to={`/farm/${b_id_farm}/${calendar}/atlas/fields`}>
Gewaspercelen
</NavLink>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<NavLink to={`/farm/${b_id_farm}/${calendar}/atlas/elevation`}>
Hoogtekaart
</NavLink>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</BreadcrumbItem>
</>
)
Expand Down
Loading