From 10ddbf53ce7598f2c0de448edce49c6e625c7fd3 Mon Sep 17 00:00:00 2001 From: Jeremy Myers Date: Thu, 19 Mar 2026 10:16:03 -0400 Subject: [PATCH] Use new hook to show loading indicator as tiles load --- src/components/OpenLayersMap.tsx | 5 ++- src/hooks/useTileLoading.ts | 69 ++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/hooks/useTileLoading.ts diff --git a/src/components/OpenLayersMap.tsx b/src/components/OpenLayersMap.tsx index e61efc4..32b482b 100644 --- a/src/components/OpenLayersMap.tsx +++ b/src/components/OpenLayersMap.tsx @@ -51,6 +51,7 @@ import { ToggleSwitch } from './ToggleSwitch'; import { CenterMapFeature } from './CenterMapFeature'; import { AperturesLayer } from './layers/AperturesLayer'; import { LoadingOverlay } from './LoadingOverlay'; +import { useTileLoading } from '../hooks/useTileLoading'; export type MapProps = { mapGroups: MapGroupResponse[]; @@ -110,6 +111,8 @@ export function OpenLayersMap({ const [isNewBoxDrawn, setIsNewBoxDrawn] = useState(false); const [isMapInitialized, setIsMapInitialized] = useState(false); + const isLoadingTiles = useTileLoading(mapRef); + const { activeBaselayer, internalBaselayers } = baselayersState; const tileLayers = useMemo(() => { @@ -484,7 +487,7 @@ export function OpenLayersMap({ externalSearchMarkerRef={externalSearchMarkerRef} isMapInitialized={isMapInitialized} /> - + ); } diff --git a/src/hooks/useTileLoading.ts b/src/hooks/useTileLoading.ts new file mode 100644 index 0000000..bacae55 --- /dev/null +++ b/src/hooks/useTileLoading.ts @@ -0,0 +1,69 @@ +import { useEffect, useState } from 'react'; +import { Map } from 'ol'; +import TileLayer from 'ol/layer/Tile'; +import XYZ from 'ol/source/XYZ'; + +export function useTileLoading(mapRef: React.RefObject) { + const [isLoadingTiles, setIsLoadingTiles] = useState(false); + + useEffect(() => { + const map = mapRef.current; + if (!map) return; + + let pendingTiles = 0; + + function onLoadStart() { + pendingTiles++; + setIsLoadingTiles(true); + } + + function onLoadEnd() { + pendingTiles = Math.max(0, pendingTiles - 1); + if (pendingTiles === 0) setIsLoadingTiles(false); + } + + // Attach to all current and future tile sources + function bindSource(layer: TileLayer) { + const source = layer.getSource(); + if (!source) return; + source.on('tileloadstart', onLoadStart); + source.on('tileloadend', onLoadEnd); + source.on('tileloaderror', onLoadEnd); // don't hang on error + } + + function unbindSource(layer: TileLayer) { + const source = layer.getSource(); + if (!source) return; + source.un('tileloadstart', onLoadStart); + source.un('tileloadend', onLoadEnd); + source.un('tileloaderror', onLoadEnd); + } + + // Bind existing layers + map.getLayers().forEach((layer) => { + if (layer instanceof TileLayer) bindSource(layer as TileLayer); + }); + + // Bind layers added after mount + const layerCollection = map.getLayers(); + layerCollection.on('add', (e) => { + if (e.element instanceof TileLayer) + bindSource(e.element as TileLayer); + }); + layerCollection.on('remove', (e) => { + if (e.element instanceof TileLayer) + unbindSource(e.element as TileLayer); + pendingTiles = 0; + setIsLoadingTiles(false); + }); + + return () => { + map.getLayers().forEach((layer) => { + if (layer instanceof TileLayer) unbindSource(layer as TileLayer); + }); + setIsLoadingTiles(false); + }; + }, [mapRef]); + + return isLoadingTiles; +}