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) {
const mapDivRef = useRef<HTMLDivElement>(null);
const { initializeMap } = useMapApi();
const mapDivRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!mapDivRef.current) return;
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 Layer from "ol/layer/Layer";
import { Source } from "ol/source";
import React, { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
/**
@@ -33,6 +34,15 @@ interface MapAPI {
*/
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
* @param name The name that will be associated with the Layer
@@ -66,35 +76,47 @@ interface Props {
children: React.ReactNode;
}
function MapAPIProvider({ children }: Props) {
export function MapAPIProvider({ children }: Props) {
const mapRef = useRef<Map | null>(null);
const [layers, setLayers] = useState<Record<string, Layer>>({});
const [isMapReady, setIsMapReady] = useState(false);
const initializeMap = useCallback((map: Map) => {
if (mapRef.current) {
throw new Error('Map has already been initialized!')
}
mapRef.current = map;
}, [mapRef.current]);
setIsMapReady(true);
}, []);
const getMap = useCallback(() => {
if (!mapRef.current) {
throw new NullMapError();
}
return mapRef.current;
}, [mapRef.current]);
const isMapReady = useMemo(
() => mapRef.current === null,
[mapRef.current]
);
}, []);
const getLayer = useCallback((name: string) => {
if (!mapRef.current) {
throw new NullMapError();
}
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) => {
if (!mapRef.current) {
@@ -105,7 +127,7 @@ function MapAPIProvider({ children }: Props) {
...prev,
[name]: layer,
}));
}, [mapRef.current]);
}, [setLayers]);
const removeLayer = useCallback((name: string) => {
if (!mapRef.current) {
@@ -122,6 +144,7 @@ function MapAPIProvider({ children }: Props) {
getMap,
isMapReady,
getLayer,
getLayerSource,
addLayer,
removeLayer,
}}>