diff --git a/src/hooks/MapContainer.tsx b/src/hooks/MapContainer.tsx index 5b0332e..66da3e7 100644 --- a/src/hooks/MapContainer.tsx +++ b/src/hooks/MapContainer.tsx @@ -7,8 +7,8 @@ interface Props { } export function MapContainer({ children }: Props) { - const mapDivRef = useRef(null); const { initializeMap } = useMapApi(); + const mapDivRef = useRef(null); useEffect(() => { if (!mapDivRef.current) return; const map = new Map({ diff --git a/src/hooks/layers/useLayerMount.ts b/src/hooks/layers/useLayerMount.ts new file mode 100644 index 0000000..5102c3c --- /dev/null +++ b/src/hooks/layers/useLayerMount.ts @@ -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; +} \ No newline at end of file diff --git a/src/hooks/layers/usePointLayer.ts b/src/hooks/layers/usePointLayer.ts new file mode 100644 index 0000000..625ed2e --- /dev/null +++ b/src/hooks/layers/usePointLayer.ts @@ -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 = (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, + }; +}; diff --git a/src/hooks/layers/vectorLayerApi.ts b/src/hooks/layers/vectorLayerApi.ts new file mode 100644 index 0000000..b58ebc8 --- /dev/null +++ b/src/hooks/layers/vectorLayerApi.ts @@ -0,0 +1,11 @@ +import { Feature } from "ol"; + +interface VectorLayerApi { + isLayerReady: boolean; + add: (features: T[]) => void; + remove: (featureIds: string[]) => void; + clear: () => void; + get: (id: string) => Feature | null | undefined; +} + +export type useVectorLayerHook = (layerName: string) => VectorLayerApi; \ No newline at end of file diff --git a/src/hooks/mapApi.tsx b/src/hooks/mapApi.tsx index 1edd5b3..8d7ff20 100644 --- a/src/hooks/mapApi.tsx +++ b/src/hooks/mapApi.tsx @@ -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(null); const [layers, setLayers] = useState>({}); + 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, }}>