Initial commit.
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
||||||
211
package-lock.json
generated
Normal file
211
package-lock.json
generated
Normal file
@@ -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=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
package.json
Normal file
21
package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/hooks/MapContainer.tsx
Normal file
28
src/hooks/MapContainer.tsx
Normal file
@@ -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<HTMLDivElement>(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 <div ref={mapDivRef}>
|
||||||
|
{children}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
131
src/hooks/mapApi.tsx
Normal file
131
src/hooks/mapApi.tsx
Normal file
@@ -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<MapAPI | null>(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<Map | null>(null);
|
||||||
|
const [layers, setLayers] = useState<Record<string, Layer>>({});
|
||||||
|
|
||||||
|
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 <ApiContext.Provider value={{
|
||||||
|
initializeMap,
|
||||||
|
getMap,
|
||||||
|
isMapReady,
|
||||||
|
getLayer,
|
||||||
|
addLayer,
|
||||||
|
removeLayer,
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</ ApiContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user