diff --git a/.gitignore b/.gitignore
index 4108b33..f019f26 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,23 @@ dist-ssr
*.njsproj
*.sln
*.sw?
+
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+# Yarn Integrity file
+.yarn-integrity
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz
deleted file mode 100644
index 8ac781b..0000000
Binary files a/.yarn/install-state.gz and /dev/null differ
diff --git a/src/App.tsx b/src/App.tsx
index d34741d..ff43058 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -6,6 +6,8 @@ import EarthquakeClusters from "./examples/EarthquakeClusters";
import { useState } from "react";
import SecondMapDialog from "./examples/SecondMapDialog";
import GridLine from "./components/map/layers/GridLine";
+import WMSLayerLoader from "./components/map/wms/WMSLayerLoader";
+import WMSExample from "./components/map/wms/WMSExample";
function App() {
const [showDialog, setShowDialog] = useState(false);
@@ -19,6 +21,8 @@ function App() {
{/* */}
{/* */}
+
+ {/* */}
{showDialog && }
diff --git a/src/components/map/layers/GridLine.tsx b/src/components/map/layers/GridLine.tsx
index 66ac1eb..3382951 100644
--- a/src/components/map/layers/GridLine.tsx
+++ b/src/components/map/layers/GridLine.tsx
@@ -3,6 +3,8 @@ import MapContext from "../context/MapContext";
import { Graticule } from "ol";
import { Stroke } from "ol/style";
+const isDebugMode = false;
+
type Props = {};
const GridLine = (props: Props) => {
@@ -21,7 +23,8 @@ const GridLine = (props: Props) => {
});
useEffect(() => {
- console.log("Adding grid line layer to map");
+ if (isDebugMode) console.log("Adding grid line layer to map");
+
if (!map) return;
map.addLayer(gridLineLayer);
diff --git a/src/components/map/wms/WMSExample.tsx b/src/components/map/wms/WMSExample.tsx
new file mode 100644
index 0000000..b57e334
--- /dev/null
+++ b/src/components/map/wms/WMSExample.tsx
@@ -0,0 +1,51 @@
+import React, { useEffect } from "react";
+import { Map, View } from "ol";
+import TileLayer from "ol/layer/Tile";
+import TileWMS from "ol/source/TileWMS";
+import { fromLonLat } from "ol/proj";
+import "ol/ol.css";
+import { OSM } from "ol/source";
+
+const WMSExample = () => {
+ useEffect(() => {
+ // Initialize the map
+ const map = new Map({
+ target: "map",
+ layers: [
+ // Base layer (optional)
+ new TileLayer({
+ source: new OSM(),
+ }),
+ ],
+ view: new View({
+ center: fromLonLat([15, 65]), // Center over Norway
+ zoom: 4,
+ }),
+ });
+
+ // Add the WMS layer
+ const wmsLayer = new TileLayer({
+ source: new TileWMS({
+ url: "https://factmaps.sodir.no/arcgis/services/FactMaps/3_0/MapServer/WMSServer",
+ params: {
+ LAYERS: "7", // 'Orthophoto' layer
+ FORMAT: "image/png",
+ TRANSPARENT: true,
+ VERSION: "1.3.0",
+ },
+ // serverType: "arcgis",
+ crossOrigin: "anonymous",
+ }),
+ });
+
+ map.addLayer(wmsLayer);
+
+ // Optional: Adjust view to layer extent (hardcoded for Norway)
+ map.getView().setCenter(fromLonLat([15, 65]));
+ map.getView().setZoom(4);
+ }, []);
+
+ return
;
+};
+
+export default WMSExample;
diff --git a/src/components/map/wms/WMSLayerLoader.tsx b/src/components/map/wms/WMSLayerLoader.tsx
new file mode 100644
index 0000000..0f2e398
--- /dev/null
+++ b/src/components/map/wms/WMSLayerLoader.tsx
@@ -0,0 +1,287 @@
+import React, { useState, useEffect } from "react";
+import TileLayer from "ol/layer/Tile";
+import TileWMS from "ol/source/TileWMS";
+import MapContext from "../context/MapContext";
+import { fromLonLat } from "ol/proj";
+
+type SelectionState = "selected" | "unselected" | "indeterminate";
+
+type WMSLayer = {
+ id: string; // Unique identifier
+ name: string;
+ title: string;
+ children?: WMSLayer[];
+ parent?: WMSLayer;
+};
+
+const defaultWMSURL =
+ "https://factmaps.sodir.no/arcgis/services/FactMaps/3_0/MapServer/WMSServer?request=GetCapabilities&service=WMS";
+
+const WMSLayerLoader = () => {
+ const { map } = React.useContext(MapContext);
+ const [wmsUrl, setWmsUrl] = useState(defaultWMSURL);
+ const [wmsVersion, setWmsVersion] = useState("1.3.0");
+ const [layers, setLayers] = useState([]);
+ const [layerSelectionState, setLayerSelectionState] = useState<{
+ [layerId: string]: SelectionState;
+ }>({});
+
+ const fetchAndParseCapabilities = async () => {
+ try {
+ const response = await fetch(wmsUrl);
+ const text = await response.text();
+ const parser = new DOMParser();
+ const xmlDoc = parser.parseFromString(text, "text/xml");
+
+ const parsedLayers = parseLayersFromCapabilities(xmlDoc);
+ setLayers(parsedLayers);
+
+ const version = getWMSVersion(xmlDoc);
+ setWmsVersion(version);
+ } catch (error) {
+ console.error("Error fetching or parsing GetCapabilities:", error);
+ }
+ };
+
+ const getWMSVersion = (xmlDoc: Document): string => {
+ const wmsCapability = xmlDoc.getElementsByTagName("WMS_Capabilities")[0];
+ if (wmsCapability) {
+ return wmsCapability.getAttribute("version") || "1.3.0";
+ }
+ const wmtMsCapabilities = xmlDoc.getElementsByTagName("WMT_MS_Capabilities")[0];
+ if (wmtMsCapabilities) {
+ return wmtMsCapabilities.getAttribute("version") || "1.1.1";
+ }
+ return "1.3.0"; // Default version
+ };
+
+ const parseLayersFromCapabilities = (xmlDoc: Document): WMSLayer[] => {
+ const capability = xmlDoc.getElementsByTagName("Capability")[0];
+ if (!capability) return [];
+
+ const topLayerNodes = Array.from(capability.childNodes).filter((node) => node.nodeName === "Layer") as Element[];
+
+ return topLayerNodes.map((node, index) => parseLayer(node, undefined, `layer-${index}`));
+ };
+
+ const parseLayer = (layerNode: Element, parentLayer?: WMSLayer, parentId: string = ""): WMSLayer => {
+ const nameNode = layerNode.getElementsByTagName("Name")[0];
+ const titleNode = layerNode.getElementsByTagName("Title")[0];
+
+ const name = nameNode?.textContent || "";
+ const title = titleNode?.textContent || "";
+
+ // Generate a unique ID by combining parent ID and current layer name or title
+ const idPart = name || title || "unnamed";
+ const id = parentId ? `${parentId}/${idPart}` : idPart;
+
+ const layer: WMSLayer = { id, name, title, parent: parentLayer };
+
+ const childLayerNodes = Array.from(layerNode.childNodes).filter((node) => node.nodeName === "Layer") as Element[];
+
+ if (childLayerNodes.length > 0) {
+ layer.children = childLayerNodes.map((childNode, index) => parseLayer(childNode, layer, `${id}`));
+ }
+
+ return layer;
+ };
+
+ const handleLayerSelection = (layer: WMSLayer, newState?: SelectionState) => {
+ const currentState = layerSelectionState[layer.id] || "unselected";
+ const nextState = newState || (currentState === "selected" ? "unselected" : "selected");
+
+ const updatedSelectionState = { ...layerSelectionState };
+ updatedSelectionState[layer.id] = nextState;
+
+ // Update children
+ if (layer.children && layer.children.length > 0) {
+ updateChildSelectionState(layer, nextState, updatedSelectionState);
+ }
+
+ // Update parents
+ updateParentSelectionState(layer.parent, updatedSelectionState);
+
+ setLayerSelectionState(updatedSelectionState);
+ };
+
+ const updateChildSelectionState = (
+ layer: WMSLayer,
+ state: SelectionState,
+ selectionState: { [layerId: string]: SelectionState }
+ ) => {
+ if (layer.children) {
+ layer.children.forEach((child) => {
+ selectionState[child.id] = state;
+ updateChildSelectionState(child, state, selectionState);
+ });
+ }
+ };
+
+ const updateParentSelectionState = (
+ layer: WMSLayer | undefined,
+ selectionState: { [layerId: string]: SelectionState }
+ ) => {
+ if (!layer) return;
+
+ const childStates = layer.children?.map((child) => selectionState[child.id]) || [];
+
+ let newState: SelectionState;
+ if (childStates.every((state) => state === "selected")) {
+ newState = "selected";
+ } else if (childStates.every((state) => state === "unselected")) {
+ newState = "unselected";
+ } else {
+ newState = "indeterminate";
+ }
+
+ selectionState[layer.id] = newState;
+
+ updateParentSelectionState(layer.parent, selectionState);
+ };
+
+ const renderLayerTree = (layers: WMSLayer[], level = 0) => {
+ return layers.map((layer) => {
+ const hasChildren = layer.children && layer.children.length > 0;
+ const paddingLeft = level * 20;
+ const selectionState = layerSelectionState[layer.id] || "unselected";
+
+ const isChecked = selectionState === "selected";
+ const isIndeterminate = selectionState === "indeterminate";
+
+ return (
+
+
+ {layer.name && (
+ handleLayerSelection(layer)}
+ label={layer.title}
+ />
+ )}
+ {!layer.name && (
+
+ {layer.title}
+
+ )}
+
+ {hasChildren && renderLayerTree(layer.children!, level + 1)}
+
+ );
+ });
+ };
+
+ const IndeterminateCheckbox = ({ id, checked, indeterminate, onChange, label }: CheckboxProps) => {
+ const ref = React.useRef(null);
+
+ useEffect(() => {
+ if (ref.current) {
+ ref.current.indeterminate = indeterminate;
+ }
+ }, [indeterminate]);
+
+ return (
+ <>
+
+
+ >
+ );
+ };
+
+ interface CheckboxProps {
+ id: string;
+ checked: boolean;
+ indeterminate: boolean;
+ onChange: () => void;
+ label: string;
+ }
+
+ useEffect(() => {
+ if (!map) return;
+
+ const selectedLayerNames = Object.entries(layerSelectionState)
+ .filter(([_, state]) => state === "selected")
+ .map(([layerId]) => {
+ // Find the layer by ID to get its name
+ const layer = findLayerById(layers, layerId);
+ return layer?.name;
+ })
+ .filter((name): name is string => !!name); // Filter out undefined names
+
+ const mapLayers = map.getLayers().getArray();
+ const existingLayerNames = mapLayers
+ .filter((layer) => layer.get("wmsLayer") === true)
+ .map((layer) => layer.get("name"));
+
+ // Remove layers that are no longer selected
+ existingLayerNames.forEach((name) => {
+ if (!selectedLayerNames.includes(name)) {
+ const layerToRemove = mapLayers.find((layer) => layer.get("name") === name);
+ if (layerToRemove) {
+ map.removeLayer(layerToRemove);
+ }
+ }
+ });
+
+ // Add new layers
+ selectedLayerNames.forEach((layerName) => {
+ if (!existingLayerNames.includes(layerName)) {
+ const wmsLayer = new TileLayer({
+ source: new TileWMS({
+ url: wmsUrl.split("?")[0],
+ params: {
+ LAYERS: layerName,
+ TILED: true,
+ VERSION: wmsVersion,
+ },
+ crossOrigin: "anonymous",
+ }),
+ opacity: 0.7,
+ zIndex: 100,
+ });
+ wmsLayer.set("name", layerName);
+ wmsLayer.set("wmsLayer", true);
+ map.addLayer(wmsLayer);
+ }
+ });
+
+ // Adjust map view
+ if (selectedLayerNames.length > 0) {
+ map.getView().setCenter(fromLonLat([15, 65]));
+ map.getView().setZoom(4);
+ }
+ }, [layerSelectionState, map, wmsUrl, wmsVersion]);
+
+ const findLayerById = (layers: WMSLayer[], id: string): WMSLayer | undefined => {
+ for (const layer of layers) {
+ if (layer.id === id) return layer;
+ if (layer.children) {
+ const childLayer = findLayerById(layer.children, id);
+ if (childLayer) return childLayer;
+ }
+ }
+ return undefined;
+ };
+
+ return (
+
+
setWmsUrl(e.target.value)}
+ placeholder="Enter WMS GetCapabilities URL"
+ />
+
+
+ {layers.length > 0 && (
+
+
Select Layers to Add:
+ {renderLayerTree(layers)}
+
+ )}
+
+ );
+};
+
+export default WMSLayerLoader;