diff --git a/src/components/StylingDrawer/SldStylingPanel/index.tsx b/src/components/StylingDrawer/SldStylingPanel/index.tsx new file mode 100644 index 000000000..d39076141 --- /dev/null +++ b/src/components/StylingDrawer/SldStylingPanel/index.tsx @@ -0,0 +1,109 @@ +import React, { + useEffect, useState, useCallback +} from 'react'; + +import { + CardStyle, CardStyleProps +} from 'geostyler/dist/Component/CardStyle/CardStyle'; +import SLDParser from 'geostyler-sld-parser'; +import { Style as GsStyle } from 'geostyler-style'; +import Layer from 'ol/layer/Layer'; +import { + ImageWMS, TileWMS +} from 'ol/source'; + +import { Logger } from '@terrestris/base-util'; +import { MapUtil } from '@terrestris/ol-util'; +import { useMap } from '@terrestris/react-util/dist/Hooks/useMap/useMap'; + +import useAppSelector from '../../../hooks/useAppSelector'; +import useSHOGunAPIClient from '../../../hooks/useSHOGunAPIClient'; +import { + fetchGeoserverStyle, fetchWorkspaceFromGetCapabilities, getLayerUrl +} from '../../../utils/geoserverUtils'; + +export type SldStylingPanelProps = CardStyleProps; + +const SldStylingPanel: React.FC = (props): JSX.Element => { + const [style, setStyle] = useState(); + const layerUid = useAppSelector(state => state.stylingDrawerLayerUid); + const map = useMap(); + const client = useSHOGunAPIClient(); + + const getStyle = useCallback(async () => { + if (!map || !layerUid) { + return; + } + + const layer = MapUtil.getLayerByOlUid(map, layerUid) as Layer; + if (!layer) { + return; + } + + const source = layer.getSource(); + if (!(source instanceof ImageWMS || source instanceof TileWMS)) { + return; + } + + const layerUrl = getLayerUrl(source); + if (!layerUrl) { + return; + } + + try { + const workspaceInfo = await fetchWorkspaceFromGetCapabilities(layerUrl, client); + if (!workspaceInfo) {return;} + + const geoserverStyle = await fetchGeoserverStyle(workspaceInfo.workspace, workspaceInfo.layerName, layerUrl, client); + if (geoserverStyle) { + const parser = new SLDParser(); + const { output: sldObject } = await parser.readStyle(geoserverStyle); + setStyle(sldObject); + } + } catch (error) { + Logger.error('Error: ', error); + } + }, [map, layerUid, client]); + + useEffect(() => { + getStyle(); + }, [getStyle]); + + useEffect(() => { + if (!map || !style) { + return; + } + + const layer = MapUtil.getLayerByOlUid(map, layerUid) as Layer; + const source = layer?.getSource() as ImageWMS; + if (!source) { + return; + } + + style.name = source.getParams().LAYERS; + + const parser = new SLDParser(); + parser.writeStyle(style).then(({ output: sld }) => { + if (sld) { + source.updateParams({ + SLD_BODY: sld, + STYLES: style.name + }); + } + }); + }, [style, map, layerUid]); + + if (!map || !style) { + return <>; + } + + return ( + + ); +}; + +export default SldStylingPanel; diff --git a/src/components/StylingDrawer/index.tsx b/src/components/StylingDrawer/index.tsx index 5be51db1d..7c93b83e4 100644 --- a/src/components/StylingDrawer/index.tsx +++ b/src/components/StylingDrawer/index.tsx @@ -1,20 +1,33 @@ -import React from 'react'; +import React, { + useEffect, useState +} from 'react'; import { Drawer, DrawerProps } from 'antd'; +import Layer from 'ol/layer/Layer'; +import { + ImageWMS, TileWMS +} from 'ol/source'; import { useTranslation } from 'react-i18next'; import './index.less'; +import { MapUtil } from '@terrestris/ol-util'; + +import { useMap } from '@terrestris/react-util'; + import useAppDispatch from '../../hooks/useAppDispatch'; import useAppSelector from '../../hooks/useAppSelector'; +import { clearStylingDrawerLayerUid } from '../../store/stylingDrawerLayerUid'; import { setStylingDrawerVisibility } from '../../store/stylingDrawerVisibility'; -import StylingComponent from '../ToolMenu/Draw/StylingDrawerButton/StylingComponent'; +import DrawLayerStylingPanel from '../ToolMenu/Draw/StylingDrawerButton/DrawLayerStylingPanel'; + +import SldStylingPanel from './SldStylingPanel'; export type StylingDrawerProps = DrawerProps; @@ -22,15 +35,39 @@ export const StylingDrawer: React.FC = ({ ...passThroughProps }): JSX.Element => { + const [isImageLayer, setIsImageLayer] = useState(); const dispatch = useAppDispatch(); const isStylingDrawerVisible = useAppSelector(state => state.stylingDrawerVisibility); + const layerUid = useAppSelector(state => state.stylingDrawerLayerUid); + const map = useMap(); const { t } = useTranslation(); + useEffect(() => { + if (!layerUid) { + setIsImageLayer(false); + } + + if (!map) { + return; + } + const layer = MapUtil.getLayerByOlUid(map, layerUid) as Layer; + if (!layer) { + return; + } + const layerSource = layer.getSource(); + if (layerSource instanceof TileWMS || layerSource instanceof ImageWMS){ + setIsImageLayer(true); + } else { + setIsImageLayer(false); + } + }, [layerUid, map]); + const onClose = () => { dispatch(setStylingDrawerVisibility(false)); + dispatch(clearStylingDrawerLayerUid()); }; return ( @@ -44,7 +81,7 @@ export const StylingDrawer: React.FC = ({ mask={false} {...passThroughProps} > - + {isImageLayer ? : } ); }; diff --git a/src/components/ToolMenu/Draw/StylingDrawerButton/StylingComponent/index.tsx b/src/components/ToolMenu/Draw/StylingDrawerButton/DrawLayerStylingPanel/index.tsx similarity index 95% rename from src/components/ToolMenu/Draw/StylingDrawerButton/StylingComponent/index.tsx rename to src/components/ToolMenu/Draw/StylingDrawerButton/DrawLayerStylingPanel/index.tsx index 4f9f3dd03..96699c173 100644 --- a/src/components/ToolMenu/Draw/StylingDrawerButton/StylingComponent/index.tsx +++ b/src/components/ToolMenu/Draw/StylingDrawerButton/DrawLayerStylingPanel/index.tsx @@ -31,9 +31,9 @@ import { useMap } from '@terrestris/react-util/dist/Hooks/useMap/useMap'; -export type StylingComponentProps = CardStyleProps; +export type DrawLayerStylingPanelProps = CardStyleProps; -export const StylingComponent: React.FC = ({ +export const DrawLayerStylingPanel: React.FC = ({ ...passThroughProps }): JSX.Element => { @@ -113,6 +113,10 @@ export const StylingComponent: React.FC = ({ const drawVectorLayer = MapUtil.getLayerByName(map, 'react-geo_digitize') as OlLayerVector; + if (!drawVectorLayer) { + return; + } + const parseStyles = async () => { let olStylePolygon: OlStyleLike; let olStyleLineString: OlStyleLike; @@ -187,4 +191,4 @@ export const StylingComponent: React.FC = ({ ); }; -export default StylingComponent; +export default DrawLayerStylingPanel; diff --git a/src/components/ToolMenu/LayerTree/LayerTreeContextMenu/index.tsx b/src/components/ToolMenu/LayerTree/LayerTreeContextMenu/index.tsx index 345f562af..6566fa55e 100644 --- a/src/components/ToolMenu/LayerTree/LayerTreeContextMenu/index.tsx +++ b/src/components/ToolMenu/LayerTree/LayerTreeContextMenu/index.tsx @@ -1,10 +1,12 @@ import React, { + useEffect, useState } from 'react'; import { faEllipsisV } from '@fortawesome/free-solid-svg-icons'; + import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -15,10 +17,7 @@ import { notification, Spin } from 'antd'; -import { - ItemType -} from 'antd/lib/menu/interface'; - +import { ItemType } from 'antd/lib/menu/interface'; import { getUid } from 'ol'; @@ -76,6 +75,9 @@ import { setLayer as setLayerDetailsLayer, show as showLayerDetailsModal } from '../../../../store/layerDetailsModal'; +import { setStylingDrawerLayerUid } from '../../../../store/stylingDrawerLayerUid'; +import { setStylingDrawerVisibility } from '../../../../store/stylingDrawerVisibility'; +import { checkIfGeoserverLayer } from '../../../../utils/geoserverUtils'; export type LayerTreeContextMenuProps = { layer: OlLayerTile | OlLayerImage; @@ -92,6 +94,7 @@ export const LayerTreeContextMenu: React.FC = ({ const [settingsVisible, setSettingsVisible] = useState(false); const [extentLoading, setExtentLoading] = useState(false); + const [isGeoserverLayer, setIsGeoserverLayer] = useState(false); const dispatch = useAppDispatch(); const client = useSHOGunAPIClient(); @@ -106,12 +109,26 @@ export const LayerTreeContextMenu: React.FC = ({ ); const metadataVisible = useAppSelector(state => state.layerTree.metadataVisible); + useEffect(() => { + if (layer) { + const layerSource = layer.getSource(); + if (layerSource) { + setIsGeoserverLayer(checkIfGeoserverLayer(layerSource)); + } else { + setIsGeoserverLayer(false); + } + } + }, [layer]); + const onContextMenuItemClick = (evt: MenuInfo): void => { if (evt?.key.startsWith('downloadLayer')) { const url = evt.key.split('|')[1]; downloadLayer(decodeURI(url)); } switch (evt?.key) { + case 'geostyler': + configureStyles(); + break; case 'zoomToExtent': zoomToLayerExtent(); break; @@ -238,6 +255,13 @@ export const LayerTreeContextMenu: React.FC = ({ a.click(); }; + const configureStyles = () => { + if (layer) { + dispatch(setStylingDrawerVisibility(true)); + dispatch(setStylingDrawerLayerUid(getUid(layer))); + } + }; + const dropdownMenuItems: ItemType[] = []; if (isWmsLayer(layer)) { @@ -299,6 +323,12 @@ export const LayerTreeContextMenu: React.FC = ({ key: 'layerDetails' }); } + if (isGeoserverLayer) { + dropdownMenuItems.push({ + label: t('LayerTreeContextMenu.styleLayer'), + key: 'geostyler' + }); + } return (
{ searchEngines, user, stylingDrawerVisibility, + stylingDrawerLayerUid, ...asyncReducers }); }; diff --git a/src/store/stylingDrawerLayerUid/index.tsx b/src/store/stylingDrawerLayerUid/index.tsx new file mode 100644 index 000000000..6e270b1a0 --- /dev/null +++ b/src/store/stylingDrawerLayerUid/index.tsx @@ -0,0 +1,25 @@ +import { + createSlice, PayloadAction +} from '@reduxjs/toolkit'; + +const initialState = ''; + +export const slice = createSlice({ + name: 'stylingDrawerLayer', + initialState, + reducers: { + setStylingDrawerLayerUid: (state, action: PayloadAction) => { + return action.payload; + }, + clearStylingDrawerLayerUid: () => { + return ''; + } + } +}); + +export const { + setStylingDrawerLayerUid, + clearStylingDrawerLayerUid +} = slice.actions; + +export default slice.reducer; diff --git a/src/utils/geoserverUtils.tsx b/src/utils/geoserverUtils.tsx new file mode 100644 index 000000000..0957f97c8 --- /dev/null +++ b/src/utils/geoserverUtils.tsx @@ -0,0 +1,121 @@ +import ImageWMS from 'ol/source/ImageWMS'; +import Source from 'ol/source/Source'; +import TileWMS from 'ol/source/TileWMS'; + +import { Logger } from '@terrestris/base-util'; +import { getBearerTokenHeader } from '@terrestris/shogun-util/dist/security/getBearerTokenHeader'; +import { SHOGunAPIClient } from '@terrestris/shogun-util/dist/service/SHOGunAPIClient'; + +export type WorkspaceInfo = { + workspace: string; + layerName: string; +}; + +export const fetchWorkspaceFromGetCapabilities = async ( + layerUrl: string, + client: SHOGunAPIClient | null +): Promise => { + try { + const url = `${layerUrl}SERVICE=WMS&REQUEST=GetCapabilities`; + const response = await fetch(url, { + headers: { + ...getBearerTokenHeader(client?.getKeycloak()) + } + }); + + const xml = await response.text(); + const parser = new DOMParser(); + const xmlDocument = parser.parseFromString(xml, 'text/xml'); + const layers = xmlDocument.getElementsByTagName('Layer'); + + let output: WorkspaceInfo | undefined; + Array.from(layers).forEach((layerElement) => { + const name = layerElement.getElementsByTagName('Name')[0]?.textContent; + + if (name) { + const [workspace, layerName] = name.split(':'); + output = { + workspace, + layerName + }; + } + }); + + return output; + } catch (error) { + Logger.error('Error: ', error); + return undefined; + } +}; + +const fetchSld = async ( + name: string, + layerUrl: string, + client: SHOGunAPIClient | null +): Promise => { + try { + const cleanLayerUrl = layerUrl.replace('/ows?', ''); + const url = `${cleanLayerUrl}/rest/styles/${name}.sld`; + + const response = await fetch(url, { + headers: { + ...getBearerTokenHeader(client?.getKeycloak()) + } + }); + + const sldData = await response.text(); + if (!sldData) { + return undefined; + } + return sldData; + } catch (error) { + Logger.error('Error: ', error); + return undefined; + } +}; + +export const fetchGeoserverStyle = async ( + layerWorkspace: string, + layerName: string, + layerUrl: string, + client: SHOGunAPIClient | null +): Promise => { + const cleanLayerUrl = layerUrl.replace('/ows?', ''); + const url = `${cleanLayerUrl}/rest/layers/${layerWorkspace}:${layerName}.json`; + + try { + const response = await fetch(url, { + headers: { + ...getBearerTokenHeader(client?.getKeycloak()) + } + }); + + const data = await response.json(); + + if (!data) { + return undefined; + } + + const style = data.layer.defaultStyle; + return await fetchSld(style.name, layerUrl, client); + } catch (error) { + Logger.error('Error: ', error); + return undefined; + } +}; + +export const getLayerUrl = (layerSource: Source | undefined): string | undefined => { + + let url: string | undefined; + if (layerSource instanceof TileWMS || layerSource instanceof ImageWMS) { + url = layerSource instanceof TileWMS ? layerSource.getUrls()?.[0] : layerSource.getUrl(); + } + return url || undefined; +}; + +export const checkIfGeoserverLayer = (layerSource: Source | undefined): boolean => { + const url = getLayerUrl(layerSource); + return url ? url.includes('/geoserver/') : false; +}; + +export default fetchWorkspaceFromGetCapabilities;