Add usePointLayer hook.

This commit is contained in:
Austin Smith
2025-12-17 08:50:13 -05:00
parent b46ba0c659
commit bfa8b166b9
5 changed files with 133 additions and 11 deletions

View File

@@ -7,8 +7,8 @@ interface Props {
} }
export function MapContainer({ children }: Props) { export function MapContainer({ children }: Props) {
const mapDivRef = useRef<HTMLDivElement>(null);
const { initializeMap } = useMapApi(); const { initializeMap } = useMapApi();
const mapDivRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
if (!mapDivRef.current) return; if (!mapDivRef.current) return;
const map = new Map({ const map = new Map({

View File

@@ -0,0 +1,20 @@
import Layer from "ol/layer/Layer";
import { useEffect, useState } from "react";
import { useMapApi } from "../mapApi";
/** Wait for map to be ready, and then add the given named layer to it.
* @returns if layer has been mounted and is ready to use
*/
export function useLayerMount(layerName: string, layer: Layer) {
const { isMapReady, addLayer } = useMapApi();
const [isLayerReady, setIsLayerReady] = useState(false);
useEffect(() => {
if (isMapReady) {
addLayer(layerName, layer);
setIsLayerReady(true);
}
}, [isMapReady]);
return isLayerReady;
}

View File

@@ -0,0 +1,68 @@
import { useVectorLayerHook } from "./vectorLayerApi";
import { useMapApi } from "../mapApi";
import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector";
import { useLayerMount } from "./useLayerMount";
import { Feature } from "ol";
import { Point } from "ol/geom";
interface PointFeature {
id?: string;
lat: number;
lon: number;
}
export const usePointLayer: useVectorLayerHook<PointFeature> = (layerName: string) => {
const { getLayerSource } = useMapApi();
const isLayerReady = useLayerMount(
layerName,
new VectorLayer({
source: new VectorSource()
})
);
const add = (points: PointFeature[]) => {
if (isLayerReady) {
const source = getLayerSource(layerName) as VectorSource;
const features = points.map((point, i) => new Feature({
id: point.id ?? i.toString(),
geometry: new Point([point.lon, point.lat]),
}));
source.addFeatures(features);
}
};
const remove = (ids: string[]) => {
if (isLayerReady) {
const source = getLayerSource(layerName) as VectorSource;
for (const id of ids) {
const feature = source.getFeatureById(id);
if (feature) {
source.removeFeature(feature);
}
}
}
};
const clear = () => {
if (isLayerReady) {
const source = getLayerSource(layerName) as VectorSource;
source.clear();
}
};
const get = (id: string) => {
if (isLayerReady) {
const source = getLayerSource(layerName) as VectorSource;
return source.getFeatureById(id);
}
};
return {
isLayerReady,
add,
remove,
clear,
get,
};
};

View File

@@ -0,0 +1,11 @@
import { Feature } from "ol";
interface VectorLayerApi<T> {
isLayerReady: boolean;
add: (features: T[]) => void;
remove: (featureIds: string[]) => void;
clear: () => void;
get: (id: string) => Feature | null | undefined;
}
export type useVectorLayerHook<T> = (layerName: string) => VectorLayerApi<T>;

View File

@@ -1,5 +1,6 @@
import { Map } from "ol"; import { Map } from "ol";
import Layer from "ol/layer/Layer"; import Layer from "ol/layer/Layer";
import { Source } from "ol/source";
import React, { createContext, useCallback, useContext, useMemo, useRef, useState } from "react"; import React, { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
/** /**
@@ -33,6 +34,15 @@ interface MapAPI {
*/ */
getLayer: (name: string) => Layer | undefined; getLayer: (name: string) => Layer | undefined;
/**
* Gets a layer's source
* @returns The Open Layers Source associated with the layer with the given name
* @throws NullMapError if map is not initialized
* @throws Error if no layer is found with the given name
* @throws Error if the layer with the given name doesn't have a Source defined.
*/
getLayerSource: (name: string) => Source;
/** /**
* Add a layer to the map * Add a layer to the map
* @param name The name that will be associated with the Layer * @param name The name that will be associated with the Layer
@@ -66,35 +76,47 @@ interface Props {
children: React.ReactNode; children: React.ReactNode;
} }
function MapAPIProvider({ children }: Props) { export function MapAPIProvider({ children }: Props) {
const mapRef = useRef<Map | null>(null); const mapRef = useRef<Map | null>(null);
const [layers, setLayers] = useState<Record<string, Layer>>({}); const [layers, setLayers] = useState<Record<string, Layer>>({});
const [isMapReady, setIsMapReady] = useState(false);
const initializeMap = useCallback((map: Map) => { const initializeMap = useCallback((map: Map) => {
if (mapRef.current) { if (mapRef.current) {
throw new Error('Map has already been initialized!') throw new Error('Map has already been initialized!')
} }
mapRef.current = map; mapRef.current = map;
}, [mapRef.current]); setIsMapReady(true);
}, []);
const getMap = useCallback(() => { const getMap = useCallback(() => {
if (!mapRef.current) { if (!mapRef.current) {
throw new NullMapError(); throw new NullMapError();
} }
return mapRef.current; return mapRef.current;
}, [mapRef.current]); }, []);
const isMapReady = useMemo(
() => mapRef.current === null,
[mapRef.current]
);
const getLayer = useCallback((name: string) => { const getLayer = useCallback((name: string) => {
if (!mapRef.current) { if (!mapRef.current) {
throw new NullMapError(); throw new NullMapError();
} }
return layers[name]; return layers[name];
}, [mapRef.current, layers]); }, [layers]);
const getLayerSource = useCallback((name: string) => {
if (!mapRef.current) {
throw new NullMapError();
}
const layer = layers[name];
if (!layer) {
throw new Error(`Layer "${name}" does not exist!`);
}
const source = layer.getSource();
if (!source) {
throw new Error(`Layer "${name}" has no Source!`);
}
return source;
}, [layers]);
const addLayer = useCallback((name: string, layer: Layer) => { const addLayer = useCallback((name: string, layer: Layer) => {
if (!mapRef.current) { if (!mapRef.current) {
@@ -105,7 +127,7 @@ function MapAPIProvider({ children }: Props) {
...prev, ...prev,
[name]: layer, [name]: layer,
})); }));
}, [mapRef.current]); }, [setLayers]);
const removeLayer = useCallback((name: string) => { const removeLayer = useCallback((name: string) => {
if (!mapRef.current) { if (!mapRef.current) {
@@ -122,6 +144,7 @@ function MapAPIProvider({ children }: Props) {
getMap, getMap,
isMapReady, isMapReady,
getLayer, getLayer,
getLayerSource,
addLayer, addLayer,
removeLayer, removeLayer,
}}> }}>