From b46ba0c65925ca1f252c3ea2a60d8b06f5983b7c Mon Sep 17 00:00:00 2001 From: Austin Smith Date: Mon, 15 Dec 2025 08:15:22 -0500 Subject: [PATCH] Initial commit. --- .gitignore | 1 + package-lock.json | 211 +++++++++++++++++++++++++++++++++++++ package.json | 21 ++++ src/hooks/MapContainer.tsx | 28 +++++ src/hooks/mapApi.tsx | 131 +++++++++++++++++++++++ 5 files changed, 392 insertions(+) create mode 100644 .gitignore create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/hooks/MapContainer.tsx create mode 100644 src/hooks/mapApi.tsx diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..198147a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,211 @@ +{ + "name": "useopenlayers", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "useopenlayers", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "ol": "^10.7.0", + "react": "^19.2.1", + "react-dom": "^19.2.1" + }, + "devDependencies": { + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "typescript": "^5.9.3" + } + }, + "node_modules/@petamoriken/float16": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz", + "integrity": "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==" + }, + "node_modules/@types/rbush": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/rbush/-/rbush-4.0.0.tgz", + "integrity": "sha512-+N+2H39P8X+Hy1I5mC6awlTX54k3FhiUmvt7HWzGJZvF+syUAAxP/stwppS8JE84YHqFgRMv6fCy31202CMFxQ==" + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "dev": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true + }, + "node_modules/earcut": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", + "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==" + }, + "node_modules/geotiff": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/geotiff/-/geotiff-2.1.3.tgz", + "integrity": "sha512-PT6uoF5a1+kbC3tHmZSUsLHBp2QJlHasxxxxPW47QIY1VBKpFB+FcDvX+MxER6UzgLQZ0xDzJ9s48B9JbOCTqA==", + "dependencies": { + "@petamoriken/float16": "^3.4.7", + "lerc": "^3.0.0", + "pako": "^2.0.4", + "parse-headers": "^2.0.2", + "quick-lru": "^6.1.1", + "web-worker": "^1.2.0", + "xml-utils": "^1.0.2", + "zstddec": "^0.1.0" + }, + "engines": { + "node": ">=10.19" + } + }, + "node_modules/lerc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz", + "integrity": "sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==" + }, + "node_modules/ol": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/ol/-/ol-10.7.0.tgz", + "integrity": "sha512-122U5gamPqNgLpLOkogFJhgpywvd/5en2kETIDW+Ubfi9lPnZ0G9HWRdG+CX0oP8od2d6u6ky3eewIYYlrVczw==", + "dependencies": { + "@types/rbush": "4.0.0", + "earcut": "^3.0.0", + "geotiff": "^2.1.3", + "pbf": "4.0.1", + "rbush": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/openlayers" + } + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" + }, + "node_modules/parse-headers": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz", + "integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==" + }, + "node_modules/pbf": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz", + "integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==", + "dependencies": { + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", + "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" + }, + "node_modules/quick-lru": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz", + "integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==" + }, + "node_modules/rbush": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-4.0.1.tgz", + "integrity": "sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ==", + "dependencies": { + "quickselect": "^3.0.0" + } + }, + "node_modules/react": { + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", + "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", + "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.1" + } + }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/web-worker": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", + "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==" + }, + "node_modules/xml-utils": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz", + "integrity": "sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA==" + }, + "node_modules/zstddec": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/zstddec/-/zstddec-0.1.0.tgz", + "integrity": "sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ba08d3c --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "useopenlayers", + "version": "1.0.0", + "description": "React wrapper over Open Layers that expose functionality via hooks.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Austin Smith", + "license": "ISC", + "dependencies": { + "ol": "^10.7.0", + "react": "^19.2.1", + "react-dom": "^19.2.1" + }, + "devDependencies": { + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "typescript": "^5.9.3" + } +} diff --git a/src/hooks/MapContainer.tsx b/src/hooks/MapContainer.tsx new file mode 100644 index 0000000..5b0332e --- /dev/null +++ b/src/hooks/MapContainer.tsx @@ -0,0 +1,28 @@ +import React, { useEffect, useRef } from "react"; +import { useMapApi } from "./mapApi"; +import { Map, View } from "ol"; + +interface Props { + children: React.ReactNode; +} + +export function MapContainer({ children }: Props) { + const mapDivRef = useRef(null); + const { initializeMap } = useMapApi(); + useEffect(() => { + if (!mapDivRef.current) return; + const map = new Map({ + target: mapDivRef.current, + layers: [], + view: new View({ + center: [0, 0], + zoom: 1, + }) + }); + initializeMap(map); + }, [mapDivRef.current, initializeMap]); + + return
+ {children} +
; +} \ No newline at end of file diff --git a/src/hooks/mapApi.tsx b/src/hooks/mapApi.tsx new file mode 100644 index 0000000..1edd5b3 --- /dev/null +++ b/src/hooks/mapApi.tsx @@ -0,0 +1,131 @@ +import { Map } from "ol"; +import Layer from "ol/layer/Layer"; +import React, { createContext, useCallback, useContext, useMemo, useRef, useState } from "react"; + +/** + * This provides the operations for manipulating the OpenLayers' map state. + */ +interface MapAPI { + /** + * The function that MUST be called when the OpenLayer's map gets mounted. + * This function should only be called once. + * @throws Error if map has already been initialized + */ + initializeMap: (map: Map) => void; + + /** + * Gets the map object + * @returns The current map reference in the context + * @throws NullMapError + */ + getMap: () => Map; + + /** + * Flags whether map functionality is available + */ + isMapReady: boolean; + + /** + * Gets a layer from the map + * @param name The name of the layer to get + * @returns The Open Layers Layer associated with the given name + * @throws NullMapError + */ + getLayer: (name: string) => Layer | undefined; + + /** + * Add a layer to the map + * @param name The name that will be associated with the Layer + * @param layer The Open Layers Layer object + * @throws NullMapError + */ + addLayer: (name: string, layer: Layer) => void; + + /** + * Remove a layer from the map + * @param name The name of the layer to remove + * @throws NullMapError + */ + removeLayer: (name: string) => void; + +} + +class NullMapError extends Error {}; + +const ApiContext = createContext(null); + +export function useMapApi() { + const context = useContext(ApiContext); + if (!context) { + throw new Error('MapAPI Context has not been provided!') + } + return context; +} + +interface Props { + children: React.ReactNode; +} + +function MapAPIProvider({ children }: Props) { + const mapRef = useRef(null); + const [layers, setLayers] = useState>({}); + + const initializeMap = useCallback((map: Map) => { + if (mapRef.current) { + throw new Error('Map has already been initialized!') + } + mapRef.current = map; + }, [mapRef.current]); + + 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]); + + const addLayer = useCallback((name: string, layer: Layer) => { + if (!mapRef.current) { + throw new NullMapError(); + } + mapRef.current.addLayer(layer); + setLayers((prev) => ({ + ...prev, + [name]: layer, + })); + }, [mapRef.current]); + + const removeLayer = useCallback((name: string) => { + if (!mapRef.current) { + throw new NullMapError(); + } + const layer = getLayer(name); + if (layer) { + mapRef.current.removeLayer(layer); + } + }, [getLayer]); + + return + {children} + ; +} +