diff --git a/.changeset/moody-berries-wave.md b/.changeset/moody-berries-wave.md new file mode 100644 index 000000000..fad036856 --- /dev/null +++ b/.changeset/moody-berries-wave.md @@ -0,0 +1,5 @@ +--- +"@nmi-agro/fdm-app": patch +--- + +The cultivation and field count list seen on field selection atlas pages now becomes scrollable when there are too many different cultivations to display, ensuring that the "Sla geselecteerde percelen op" button is always reachable. diff --git a/fdm-app/app/components/blocks/atlas/atlas-panels.tsx b/fdm-app/app/components/blocks/atlas/atlas-panels.tsx index c5e0632cc..2fa0d7309 100644 --- a/fdm-app/app/components/blocks/atlas/atlas-panels.tsx +++ b/fdm-app/app/components/blocks/atlas/atlas-panels.tsx @@ -1,8 +1,8 @@ import type { FeatureCollection } from "geojson" import throttle from "lodash.throttle" -import { Check, Info } from "lucide-react" -import type { MapLibreZoomEvent } from "maplibre-gl" -import { useCallback, useEffect, useState } from "react" +import { Check, ChevronDown, ChevronUp, Info } from "lucide-react" +import type { MapGeoJSONFeature, MapLibreZoomEvent } from "maplibre-gl" +import { useCallback, useEffect, useRef, useState } from "react" import type { MapLayerMouseEvent as MapMouseEvent } from "react-map-gl/maplibre" import { useMap } from "react-map-gl/maplibre" import { data, NavLink, useFetcher } from "react-router" @@ -18,6 +18,7 @@ import { CardTitle, } from "~/components/ui/card" import { Spinner } from "~/components/ui/spinner" +import { Separator } from "~/components/ui/separator" import { cn } from "~/lib/utils" export function FieldsPanelHover({ @@ -27,29 +28,47 @@ export function FieldsPanelHover({ clickRedirectsToDetailsPage = false, }: { zoomLevelFields: number - layer: string + layer: string[] | string layerExclude?: string[] | string clickRedirectsToDetailsPage?: boolean }) { const { current: map } = useMap() const [panel, setPanel] = useState(null) + const layerIds = Array.isArray(layer) ? layer : [layer] + const excludedLayerIds = layerExclude + ? Array.isArray(layerExclude) + ? layerExclude + : [layerExclude] + : [] + const layerIdsKey = layerIds.join("|") + const excludedLayerIdsKey = excludedLayerIds.join("|") + + // biome-ignore lint/correctness/useExhaustiveDependencies: effective changes in layer and layerExclude are detected through layerIdsKey and excludedLayerIdsKey useEffect(() => { function updatePanel(evt: MapMouseEvent | MapLibreZoomEvent) { if (map) { // Set message about zoom level const zoom = map.getZoom() if (zoom && zoom > zoomLevelFields) { - if (!map.getStyle() || !map.getLayer(layer)) return - + if (!map.getStyle()) return + if (!("point" in evt)) { + setPanel(makePanel({})) + return + } + const validLayers = layerIds.filter((l) => map.getLayer(l)) + if (validLayers.length === 0) return const features = map.queryRenderedFeatures(evt.point, { - layers: [layer], + layers: validLayers, }) + // Layer, whose id is specified last in the layer prop, has the highest priority + features.sort( + (f1, f2) => + validLayers.indexOf(f2.layer.id) - + validLayers.indexOf(f1.layer.id), + ) if (layerExclude) { - const layers = Array.isArray(layerExclude) - ? layerExclude - : [layerExclude] - const validLayers = layers.filter((l) => + const validLayers = excludedLayerIds.filter((l) => map.getLayer(l), ) @@ -61,25 +80,40 @@ export function FieldsPanelHover({ }, ) if (featuresExclude && featuresExclude.length > 0) { - setPanel(null) + setPanel(makePanel({})) return } } } - if ( - features && - features.length > 0 && - features[0].properties - ) { + const top = features[0] + if (top?.properties) { setPanel( - + makePanel({ layer: top.layer.id, feature: top }), + ) + } else { + setPanel(makePanel({})) + } + + function makePanel({ + layer, + feature, + }: { + layer?: string + feature?: MapGeoJSONFeature + }) { + const active = layer && feature + const name = feature + ? layer === "fieldsSaved" + ? feature.properties.b_name + : feature.properties.b_lu_name + : "Naam" + return ( + - - {layer === "fieldsSaved" - ? features[0].properties.b_name - : features[0].properties.b_lu_name} - + {name} {layer === "fieldsSaved" ? `${features[0].properties.b_area} ha` @@ -90,32 +124,63 @@ export function FieldsPanelHover({ : "Klik om te verwijderen"} - , + ) - } else { - setPanel(null) } + } else { + setPanel(null) } } } - const throttledUpdatePanel = throttle(updatePanel, 250, { - trailing: true, - }) + // Throttle panel updates to not overwhelm React, the rendering thread etc. + const throttleInterval = 200 + const throttledUpdatePanelInner = throttle( + updatePanel, + throttleInterval, + { + trailing: true, + }, + ) + + // Delay handling of clicks so that if the field selection under the mouse changes we catch it + let delayedUpdateTimeout: ReturnType + const delayedUpdatePanel: typeof updatePanel = (e) => { + delayedUpdateTimeout = setTimeout( + () => throttledUpdatePanelInner(e), + throttleInterval, + ) + } + + // Cancels any timed out invocations and tries to invoke again + const throttledUpdatePanel: typeof updatePanel = (e) => { + clearTimeout(delayedUpdateTimeout) + throttledUpdatePanelInner(e) + } if (map) { map.on("mousemove", throttledUpdatePanel) - map.on("click", updatePanel) + map.on("mousedown", delayedUpdatePanel) map.on("zoom", throttledUpdatePanel) - map.on("load", updatePanel) + map.once("load", updatePanel) return () => { map.off("mousemove", throttledUpdatePanel) - map.off("click", updatePanel) + map.off("mousedown", delayedUpdatePanel) map.off("zoom", throttledUpdatePanel) map.off("load", updatePanel) + + // Cancel pending updates + clearTimeout(delayedUpdateTimeout) + throttledUpdatePanelInner.cancel() } } - }, [map, zoomLevelFields, layer, layerExclude, clickRedirectsToDetailsPage]) + }, [ + map, + zoomLevelFields, + layerIdsKey, + excludedLayerIdsKey, + clickRedirectsToDetailsPage, + ]) return panel } @@ -149,7 +214,7 @@ export function FieldsPanelZoom({ } } - const throttledUpdatePanel = throttle(updatePanel, 250, { + const throttledUpdatePanel = throttle(updatePanel, 200, { trailing: true, }) @@ -179,6 +244,8 @@ export function FieldsPanelSelection({ const fetcher = useFetcher() const { current: map } = useMap() const [panel, setPanel] = useState(null) + const scrollContainerRef = useRef(null) + const scrollRef = useRef(null) const isSubmitting = fetcher.state !== "idle" @@ -230,19 +297,18 @@ export function FieldsPanelSelection({ }[], feature, ) => { - if (!feature.properties) return acc + const cropField = feature.properties + if (!cropField) return acc const existingCultivation = acc.find( - (c) => - c.b_lu_name === - feature.properties.b_lu_name, + (c) => c.b_lu_name === cropField.b_lu_name, ) if (existingCultivation) { existingCultivation.count++ } else { acc.push({ - b_lu_name: feature.properties.b_lu_name, + b_lu_name: cropField.b_lu_name, b_lu_croprotation: - feature.properties.b_lu_croprotation, + cropField.b_lu_croprotation, count: 1, }) } @@ -252,45 +318,70 @@ export function FieldsPanelSelection({ ) setPanel( - - + + Percelen {fieldCountText} - -
- {cultivations.map((cultivation, _index) => ( - // let cultivationCountText = `${cultivation.count + 1} percelen` - -
- -
-

- {cultivation.b_lu_name} -

-

- {`${cultivation.count} percelen`} -

-
-
- ))} + + {/* Top scroll indicator */} +
+ + +
+ +
+
+ {cultivations.map( + (cultivation, _index) => ( + // let cultivationCountText = `${cultivation.count + 1} percelen` + +
+ +
+

+ { + cultivation.b_lu_name + } +

+

+ {`${cultivation.count} percelen`} +

+
+
+ ), + )} +
+
+ + {/* Bottom scroll indicator */} +
+ +