diff --git a/src/client/map/MapSet/MapTooltip/MapTooltip.tsx b/src/client/map/MapSet/MapTooltip/MapTooltip.tsx index 1671438..b86316c 100644 --- a/src/client/map/MapSet/MapTooltip/MapTooltip.tsx +++ b/src/client/map/MapSet/MapTooltip/MapTooltip.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { TooltipAttribute } from 'src/client/story/utils/getTooltipAttributes'; import './MapTooltip.css'; +import { TooltipAttribute } from '../../../shared/models/models.tooltip'; /** * Props for the MapTooltip component. diff --git a/src/client/map/MapSet/MapTooltip/getMapTooltip.ts b/src/client/map/MapSet/MapTooltip/getMapTooltip.ts index eed2ecc..9647259 100644 --- a/src/client/map/MapSet/MapTooltip/getMapTooltip.ts +++ b/src/client/map/MapSet/MapTooltip/getMapTooltip.ts @@ -1,7 +1,8 @@ import { PickingInfo } from '@deck.gl/core'; import { RenderingLayer } from '../../../shared/models/models.layers'; import { parseDatasourceConfiguration } from '../../../shared/models/parsers.datasources'; -import { getTooltipAttributes, TooltipAttribute } from '../../../story/utils/getTooltipAttributes'; +import { getTooltipAttributes } from '../../../story/utils/getTooltipAttributes'; +import { TooltipAttribute } from '../../../shared/models/models.tooltip'; import './getMapTooltip.css'; /** diff --git a/src/client/map/MapSet/SingleMap.tsx b/src/client/map/MapSet/SingleMap.tsx index 748a8e2..4ffe8d5 100644 --- a/src/client/map/MapSet/SingleMap.tsx +++ b/src/client/map/MapSet/SingleMap.tsx @@ -1,21 +1,21 @@ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { DeckGL } from '@deck.gl/react'; -import { LayersList, PickingInfo, ViewStateChangeParameters } from '@deck.gl/core'; +import { PickingInfo, ViewStateChangeParameters } from '@deck.gl/core'; import { useSharedState } from '../../shared/hooks/state.useSharedState'; import { getMapByKey } from '../../shared/appState/selectors/getMapByKey'; import { MapView } from '../../shared/models/models.mapView'; -import { mergeViews } from '../../map/logic/mapView/mergeViews'; -import { ActionMapViewChange } from '../../shared/appState/state.models.actions'; -import { getViewChange } from '../../map/logic/mapView/getViewChange'; +import { StateActionType } from '../../shared/appState/enum.state.actionType'; import { getLayersByMapKey } from '../../shared/appState/selectors/getLayersByMapKey'; -import { parseLayersFromSharedState } from '../../map/logic/parsing.layers'; -import { getSelectionByKey } from '../../shared/appState/selectors/getSelectionByKey'; +import { ActionMapViewChange } from '../../shared/appState/state.models.actions'; +import { mergeViews } from '../logic/mapView/mergeViews'; +import { getViewChange } from '../logic/mapView/getViewChange'; import { handleMapClick } from './handleMapClick'; -import { StateActionType } from '../../shared/appState/enum.state.actionType'; import { handleMapHover } from './handleMapHover'; import { getMapTooltip } from './MapTooltip/getMapTooltip'; import { MapTooltip } from './MapTooltip/MapTooltip'; -import { TooltipAttribute } from '../../story/utils/getTooltipAttributes'; +import { LayerInstance, LayerManager } from '../components/layers/LayerManager'; +import { RenderingLayer } from '../../shared/models/models.layers'; +import { TooltipAttribute } from '../../shared/models/models.tooltip'; const TOOLTIP_VERTICAL_OFFSET_CURSOR_POINTER = 10; const TOOLTIP_VERTICAL_OFFSET_CURSOR_GRABBER = 20; @@ -29,6 +29,8 @@ export interface BasicMapProps { CustomTooltip?: React.ElementType | boolean; } +type LayerRegistry = Record; + /** * SingleMap component intended to be used in MapSet component. * @@ -46,22 +48,28 @@ export const SingleMap = ({ mapKey, syncedView, CustomTooltip = false }: BasicMa /** Get the current map state and layers from shared state */ const mapState = getMapByKey(sharedState, mapKey); const mapViewState = mergeViews(syncedView, mapState?.view ?? {}); - const mapLayers = getLayersByMapKey(sharedState, mapKey); + const mapLayers = getLayersByMapKey(sharedState, mapKey) ?? []; + + // Local registry for actual Deck.gl class instances + const [layerRegistry, setLayerRegistry] = useState({}); + + const handleLayerUpdate = useCallback((id: string, instance: LayerInstance) => { + setLayerRegistry((prev: LayerRegistry) => { + if (prev[id] === instance) return prev; // Avoid unnecessary re-renders + return { ...prev, [id]: instance }; + }); + }, []); + + // Filter and sort layers based on the order in mapLayers + const activeLayers: LayerInstance[] = useMemo(() => { + return mapLayers + .map((layer: RenderingLayer) => layerRegistry[layer.key]) + .filter((layer: LayerInstance) => layer !== null && layer !== undefined); + }, [mapLayers, layerRegistry]); /** Determines if custom tooltip logic should be used */ const useCustomTooltip = Boolean(CustomTooltip); - /** - * Returns the selection object for a given selectionKey. - * This is a selector callback passed to layer parsing logic. - * - * @param {string} selectionKey - The key identifying the selection. - * @returns {Selection | undefined} The selection object, or undefined if not found. - */ - const getSelection = (selectionKey: string) => { - return getSelectionByKey(sharedState, selectionKey); - }; - /** * On mount: sync the map view and set up keyboard listeners for Ctrl key. */ @@ -119,14 +127,6 @@ export const SingleMap = ({ mapKey, syncedView, CustomTooltip = false }: BasicMa }); }; - /** Parse layers for DeckGL rendering */ - const layers: LayersList = mapLayers - ? parseLayersFromSharedState({ - sharedStateLayers: [...mapLayers], - getSelectionForLayer: getSelection, - }) - : []; - /** * Handles changes to the map view state (e.g., pan, zoom). * @@ -153,9 +153,10 @@ export const SingleMap = ({ mapKey, syncedView, CustomTooltip = false }: BasicMa return ( <> + ; metersPerUnit: Array }) => void; } -/** Rendered map with DeckGL tool used as a geospatial renderer */ +/** Rendered map with DeckGL tool used as a geospatial renderer + * @deprecated Use MapSet and related components instead. + * */ export const RenderingMap: React.FC = (props: RenderMapProps) => { // shared application state in context const [sharedState] = useSharedState(); diff --git a/src/client/map/components/layers/COGLayerSource.tsx b/src/client/map/components/layers/COGLayerSource.tsx new file mode 100644 index 0000000..4bdac21 --- /dev/null +++ b/src/client/map/components/layers/COGLayerSource.tsx @@ -0,0 +1,75 @@ +import React, { useEffect, useMemo } from 'react'; +import geolib from '@gisatcz/deckgl-geolib'; +import { Layer } from '@deck.gl/core'; +import { LayerSourceProps } from './LayerManager'; +import { parseDatasourceConfiguration } from '../../../shared/models/parsers.datasources'; + +const CogBitmapLayer = geolib.CogBitmapLayer; + +/** + * A React component that creates and manages a COG (Cloud Optimized GeoTIFF) layer. + * This component uses the `CogBitmapLayer` from `@gisatcz/deckgl-geolib` to render raster data. + * + * @param {LayerSourceProps} props - The props for the COGLayerSource component. + * @param {RenderingLayer} props.layer - The layer configuration object. + * @param {(id: string, instance: Layer | null) => void} props.onLayerUpdate - Callback to handle updates to the layer instance. + * @returns {null} This component does not render any DOM elements. + */ +export const COGLayerSource = React.memo(({ layer, onLayerUpdate }: LayerSourceProps) => { + // Destructure properties from the layer configuration + const { isActive, key, opacity, datasource } = layer; + const { url, configuration } = datasource; + + // Ensure the URL is provided in the datasource + if (!url) { + throw new Error(`COGLayerSource: Missing url in datasource: ${key}`); + } + + // Parse the datasource configuration + const config = parseDatasourceConfiguration(configuration); + if (!config) { + // Log a warning if the configuration is missing + console.warn(`COGLayerSource: Missing configuration in datasource: ${key}`); + } + + // Extract COG bitmap options from the parsed configuration + const cogBitmapOptions = config?.cogBitmapOptions; + if (!cogBitmapOptions) { + // Log a warning if the COG bitmap options are missing + console.warn(`COGLayerSource: Missing cogBitmapOptions in datasource configuration: ${key}`); + } + + /** + * Memoize the creation of the CogBitmapLayer instance to avoid unnecessary re-renders. + * The layer instance is recreated only when its dependencies change. + */ + const layerInstance: Layer = useMemo(() => { + if (!cogBitmapOptions) { + return null; + } + return new CogBitmapLayer({ + id: key, + rasterData: url, + isTiled: true, + opacity: opacity ?? 1, + visible: isActive, + cogBitmapOptions, + }); + /* TODO: Since cogBitmapOptions is derived from configuration, which originally is a string + (from ptr-be-core model HasConfiguration) and later parsed to an object, + we need to stringify it here to avoid infinite render loops due to object reference changes. */ + }, [url, isActive, key, opacity, JSON.stringify(cogBitmapOptions)]); + + /** + * Effect hook to handle layer updates. + * The `onLayerUpdate` callback is called with the layer instance when the component mounts + * and with `null` when the component unmounts. + */ + useEffect(() => { + onLayerUpdate(key, layerInstance); + return () => onLayerUpdate(key, null); // Cleanup on unmount + }, [layerInstance, key, onLayerUpdate]); + + // This component does not render any DOM elements + return null; +}); diff --git a/src/client/map/components/layers/GeojsonLayerSource.tsx b/src/client/map/components/layers/GeojsonLayerSource.tsx new file mode 100644 index 0000000..42bcb3e --- /dev/null +++ b/src/client/map/components/layers/GeojsonLayerSource.tsx @@ -0,0 +1,177 @@ +import React, { useEffect, useMemo } from 'react'; +import { GeoJsonLayer } from '@deck.gl/layers'; +import { SELECTION_DEFAULT_COLOUR } from '../../../shared/constants/colors'; +import { getFeatureId } from '../../../shared/helpers/getFeatureId'; +import { hexToRgbArray } from '../../../shared/helpers/hexToRgbArray'; +import { useSharedState } from '../../../shared/hooks/state.useSharedState'; +import { getSelectionByKey } from '../../../shared/appState/selectors/getSelectionByKey'; +import { useAxios } from '../../../shared/hooks/useAxios'; +import { LayerSourceProps } from './LayerManager'; +import { parseDatasourceConfiguration } from '../../../shared/models/parsers.datasources'; + +/** + * Represents the structure needed for feature identification and property access. + */ +interface Feature { + type: 'Feature'; + id?: string; + properties?: { [key: string]: string }; +} + +/** + * Default layer style for GeoJsonLayer rendering. + */ +const defaultLayerStyle = { + filled: true, + stroked: true, + pickable: false, + pointRadiusScale: 0.2, + getPointRadius: 50, + getFillColor: [255, 255, 255], + getLineColor: [0, 0, 0, 100], + getLineWidth: 1, + lineWidthUnits: 'pixels' as const, +}; + +/** + * A React component that creates and manages a GeoJSON layer. + * This component uses the `GeoJsonLayer` from `@deck.gl/layers` to render GeoJSON data. + * + * @param {LayerSourceProps} props - The props for the GeojsonLayerSource component. + * @param {RenderingLayer} props.layer - The layer configuration object. + * @param {(id: string, instance: GeoJsonLayer | null) => void} props.onLayerUpdate - Callback to handle updates to the layer instance. + * @returns {null} This component does not render any DOM elements. + */ +export const GeojsonLayerSource = React.memo(({ layer, onLayerUpdate }: LayerSourceProps) => { + const [sharedState] = useSharedState(); + const { isActive, key, opacity, datasource, isInteractive, selectionKey, fetchOptions } = layer; + const { url, documentId, validIntervalIso, configuration } = datasource; + const route = fetchOptions?.route; + const method = fetchOptions?.method; + + // We need a fallback URL if no route is provided (e.g. external static GeoJSON file) + if (!url && !route) { + throw new Error(`GeojsonLayerSource: Missing both route and url in datasource: ${key}`); + } + + // Log a warning if the documentId is missing + if (!documentId) { + console.warn(`GeojsonLayerSource: Missing documentId in datasource: ${key}`); + } + + // Parse the datasource configuration + const config = parseDatasourceConfiguration(configuration); + + // Extract GeoJSON options from the parsed configuration + // TODO geojsonOptions are currently used for styling and featureIdProperty, solve this properly in the future + const geojsonOptions = config?.geojsonOptions; + if (!geojsonOptions) { + console.warn(`GeojsonLayerSource: Missing geojsonOptions in datasource configuration: ${key}`); + } + + // Layer style + const layerStyle = geojsonOptions?.layerStyle ?? defaultLayerStyle; + + // Selection + const selection = selectionKey ? getSelectionByKey(sharedState, selectionKey) : null; + const selectedFeatureKeys = selection?.featureKeys ?? []; + const distinctColours = selection?.distinctColours ?? [SELECTION_DEFAULT_COLOUR]; + const featureKeyColourIndexPairs = selection?.featureKeyColourIndexPairs ?? {}; + + // Load the data from route + let { + data: fetchedData, + error, + isLoading, + } = useAxios( + { fetchUrl: route }, + undefined, + { documentId: documentId, validIntervalIso, url }, + { method, skip: !route } + ); + + /** + * DATA LOGIC: + * 1. If route is provided and data is successfully fetched, use fetchedData. + * 2. If no route is provided OR there is an error fetching from the route, fallback to url. + */ + const data = route && fetchedData && !error ? fetchedData : url; + + // We only show "loading" if we are actively trying to fetch from a route and haven't failed yet + const isDataLoading = !!route && isLoading && !error; + + /** + * Returns the line color for a feature. + * If the feature is selected, returns its assigned color; otherwise, returns the default. + * + * @param {Feature} feature - The GeoJSON feature object. + * @returns {number[]} The RGBA color array for the feature's line. + */ + function getLineColor(feature: Feature): number[] { + const featureId = getFeatureId(feature, geojsonOptions?.featureIdProperty); + if (featureId && selectedFeatureKeys.includes(featureId)) { + const colourIndex = featureKeyColourIndexPairs[featureId]; + const hex = distinctColours[colourIndex] ?? distinctColours[0]; + // Convert hex to RGB array and add alpha channel + return [...hexToRgbArray(hex), 255]; + } + return layerStyle.getLineColor; + } + + /** + * Returns the line width for a feature. + * If the feature is selected, returns a thicker line; otherwise, returns the default. + * + * @param {Feature} feature - The GeoJSON feature object. + * @returns {number} The width of the feature's line. + */ + function getLineWidth(feature: Feature): number { + const featureId = getFeatureId(feature, geojsonOptions?.featureIdProperty); + if (featureId && selectedFeatureKeys.includes(featureId)) { + return 5; + } + return layerStyle.getLineWidth; + } + + /** + * Memoize the creation of the GeoJsonLayer instance to avoid unnecessary re-renders. + * The layer instance is recreated only when its dependencies change. + */ + const layerInstance: GeoJsonLayer | null = useMemo(() => { + // Prevent rendering only if we are in the middle of a route fetch. + // If we have no route, or we have an error, we proceed with 'url' as data. + if (isDataLoading || !data) { + return null; + } + + return new GeoJsonLayer({ + id: key, + opacity: opacity ?? 1, + visible: isActive, + data, + updateTriggers: { + getLineColor: [layerStyle, selection], + getFillColor: [layerStyle, selection], + getLineWidth: [layerStyle, selection], + pickable: [layerStyle, isInteractive], + }, + ...layerStyle, + getLineColor, + getLineWidth, + pickable: isInteractive ?? layerStyle.pickable, + }); + }, [isActive, key, opacity, isInteractive, layerStyle, selection, data, geojsonOptions, isDataLoading]); + + /** + * Effect hook to handle layer updates. + * The `onLayerUpdate` callback is called with the layer instance when the component mounts + * and with `null` when the component unmounts. + */ + useEffect(() => { + onLayerUpdate(key, layerInstance); + return () => onLayerUpdate(key, null); // cleanup on unmount + }, [layerInstance, key, onLayerUpdate]); + + // This component does not render any DOM elements + return null; +}); diff --git a/src/client/map/components/layers/LayerManager.tsx b/src/client/map/components/layers/LayerManager.tsx new file mode 100644 index 0000000..86e09f6 --- /dev/null +++ b/src/client/map/components/layers/LayerManager.tsx @@ -0,0 +1,75 @@ +import { UsedDatasourceLabels } from '@gisatcz/ptr-be-core/browser'; +import { MVTLayer, TileLayer, _WMSLayer as WMSLayer } from '@deck.gl/geo-layers'; +import { GeoJsonLayer } from '@deck.gl/layers'; +import { Layer } from '@deck.gl/core'; +import { RenderingLayer } from '../../../shared/models/models.layers'; +import { XYZLayerSource } from './XYZLayerSource'; +import { COGLayerSource } from './COGLayerSource'; +import { GeojsonLayerSource } from './GeojsonLayerSource'; +import { WMSLayerSource } from './WMSLayerSource'; + +/** + * Represents the possible types of layer instances that can be managed. + * This includes specific layer types from the `deck.gl` library. + */ +export type LayerInstance = TileLayer | GeoJsonLayer | WMSLayer | MVTLayer | Layer | null; + +/** + * Props for the `LayerManager` component. + * @property {RenderingLayer[]} layers - Array of layers to be rendered and managed. + * @property {(id: string, instance: LayerInstance) => void} onLayerUpdate - Callback function to handle updates to layer instances. + */ +interface LayerManagerProps { + layers: RenderingLayer[]; + onLayerUpdate: (id: string, instance: LayerInstance) => void; +} + +/** + * Props for individual layer source components. + * @property {RenderingLayer} layer - The configuration object for the layer. + * @property {(id: string, instance: LayerInstance) => void} onLayerUpdate - Callback function to handle updates to the layer instance. + */ +export interface LayerSourceProps { + layer: RenderingLayer; + onLayerUpdate: (id: string, instance: LayerInstance) => void; +} + +/** + * The `LayerManager` component is responsible for rendering and managing multiple layers. + * It dynamically selects the appropriate layer source component based on the datasource labels. + * + * @param {LayerManagerProps} props - The props for the `LayerManager` component. + * @returns {JSX.Element} A React fragment containing the rendered layer components. + */ +export const LayerManager = ({ layers, onLayerUpdate }: LayerManagerProps) => { + return ( + <> + {layers.map((layer) => { + // Extract datasource labels from the layer + const labels: string[] = layer?.datasource?.labels; + + // Log an error if no labels are provided for the layer + if (!labels?.length) { + // Log it instead of throwing to keep the React tree stable + console.error(`Datasource error: Missing labels for layer ${layer.key}`); + return null; + } + + // Render the appropriate layer source component based on the datasource labels + if (labels.includes(UsedDatasourceLabels.XYZ)) { + return ; + } else if (labels.includes(UsedDatasourceLabels.COG)) { + return ; + } else if (labels.includes(UsedDatasourceLabels.WMS)) { + return ; + } else if (labels.includes(UsedDatasourceLabels.Geojson)) { + return ; + } else { + // Log a warning if the datasource type is unknown + console.warn(`Datasource Warning - Unknown datasource type for layer ${layer.key}`); + return null; + } + })} + + ); +}; diff --git a/src/client/map/components/layers/WMSLayerSource.tsx b/src/client/map/components/layers/WMSLayerSource.tsx new file mode 100644 index 0000000..34a3758 --- /dev/null +++ b/src/client/map/components/layers/WMSLayerSource.tsx @@ -0,0 +1,69 @@ +import React, { useEffect, useMemo } from 'react'; +import { _WMSLayer as WMSLayer } from '@deck.gl/geo-layers'; +import { Layer } from '@deck.gl/core'; +import { LayerSourceProps } from './LayerManager'; +import { parseDatasourceConfiguration } from '../../../shared/models/parsers.datasources'; + +/** + * A React component that creates and manages a WMS (Web Map Service) layer. + * This component uses the `@deck.gl/geo-layers` library to render the WMS layer. + * + * @param {LayerSourceProps} props - The props for the WMSLayerSource component. + * @param {RenderingLayer} props.layer - The layer configuration object. + * @param {(id: string, instance: Layer | null) => void} props.onLayerUpdate - Callback to handle updates to the layer instance. + * @returns {null} This component does not render any DOM elements. + */ +export const WMSLayerSource = React.memo(({ layer, onLayerUpdate }: LayerSourceProps) => { + const { isActive, key, opacity, datasource } = layer; + const { url, configuration } = datasource; + + // Ensure the URL is provided in the datasource + if (!url) { + throw new Error(`WMSLayerSource: Missing url in datasource: ${key}`); + } + + // Parse the datasource configuration + const config = parseDatasourceConfiguration(configuration); + if (!config) { + throw new Error(`WMSLayerSource: Missing configuration in datasource: ${key}`); + } + + // Extract sublayers from the parsed configuration + const sublayers = config?.sublayers; + if (!sublayers) { + console.warn(`WMSLayerSource: Missing sublayers in datasource configuration: ${key}`); + } + + /** + * Memoize the creation of the WMS layer instance to avoid unnecessary re-renders. + * The layer instance is recreated only when its dependencies change. + */ + const layerInstance: Layer = useMemo(() => { + return new WMSLayer({ + id: key, + visible: isActive, + data: url, + minZoom: 0, + maxZoom: 16, + opacity: opacity ?? 1, + layers: sublayers, + serviceType: 'wms', + }); + /* TODO: Since sublayers are derived from configuration, which originally is a string + (from ptr-be-core model HasConfiguration) and later parsed to an object, + we need to stringify it here to avoid infinite render loops due to object reference changes. */ + }, [url, isActive, key, opacity, JSON.stringify(sublayers)]); + + /** + * Effect hook to handle layer updates. + * The `onLayerUpdate` callback is called with the layer instance when the component mounts + * and with `null` when the component unmounts. + */ + useEffect(() => { + onLayerUpdate(key, layerInstance); + return () => onLayerUpdate(key, null); // Cleanup on unmount + }, [layerInstance, key, onLayerUpdate]); + + // This component does not render any DOM elements + return null; +}); diff --git a/src/client/map/components/layers/XYZLayerSource.tsx b/src/client/map/components/layers/XYZLayerSource.tsx new file mode 100644 index 0000000..77cc6a4 --- /dev/null +++ b/src/client/map/components/layers/XYZLayerSource.tsx @@ -0,0 +1,77 @@ +import React, { useEffect, useMemo } from 'react'; +import { BitmapLayer } from '@deck.gl/layers'; +import { TileLayer } from '@deck.gl/geo-layers'; +import { LayerSourceProps } from './LayerManager'; + +/** + * A React component that creates and manages an XYZ tile layer. + * This component uses the `TileLayer` from `@deck.gl/geo-layers` to render tiles + * and the `BitmapLayer` to render individual tile images. + * + * @param {LayerSourceProps} props - The props for the XYZLayerSource component. + * @param {RenderingLayer} props.layer - The layer configuration object. + * @param {(id: string, instance: TileLayer | null) => void} props.onLayerUpdate - Callback to handle updates to the layer instance. + * @returns {null} This component does not render any DOM elements. + */ +export const XYZLayerSource = React.memo(({ layer, onLayerUpdate }: LayerSourceProps) => { + // Destructure properties from the layer configuration + const { isActive, key, opacity, datasource } = layer; + + // Extract the URL from the datasource + const url = datasource?.url; + if (!url) { + throw new Error(`XYZLayerSource: Missing url in datasource: ${key}`); + } + + /** + * Memoize the creation of the TileLayer instance to avoid unnecessary re-renders. + * The layer instance is recreated only when its dependencies change. + */ + const layerInstance: TileLayer = useMemo(() => { + return new TileLayer({ + id: key, + visible: isActive, + opacity: opacity ?? 1, + data: url, + minZoom: 0, + maxZoom: 19, // Possibly higher zoom levels can be supported in the future + tileSize: 256, + maxRequests: 20, + pickable: true, + /** + * Function to render sublayers for each tile. + * Creates a `BitmapLayer` for rendering the tile image. + * + * @param {Object} props - Properties for the sublayer. + * @param {Object} props.tile - The tile object containing bounding box information. + * @param {Array} props.tile.boundingBox - The bounding box of the tile as [[west, south], [east, north]]. + * @param {string} props.data - The URL of the tile image. + * @returns {BitmapLayer[]} An array containing the `BitmapLayer` for the tile. + */ + renderSubLayers: (props) => { + const [[west, south], [east, north]] = props.tile.boundingBox; + const { data, ...otherProps } = props; + + return [ + new BitmapLayer(otherProps, { + image: data, + bounds: [west, south, east, north], + }), + ]; + }, + }); + }, [url, isActive, key, opacity]); + + /** + * Effect hook to handle layer updates. + * The `onLayerUpdate` callback is called with the layer instance when the component mounts + * and with `null` when the component unmounts. + */ + useEffect(() => { + onLayerUpdate(key, layerInstance); + return () => onLayerUpdate(key, null); // Cleanup on unmount + }, [layerInstance, key, onLayerUpdate]); + + // This component does not render any DOM elements + return null; +}); diff --git a/src/client/map/logic/map.layers.cog.ts b/src/client/map/logic/map.layers.cog.ts index 57437b5..c27a74d 100644 --- a/src/client/map/logic/map.layers.cog.ts +++ b/src/client/map/logic/map.layers.cog.ts @@ -6,6 +6,7 @@ import { UsedDatasourceLabels } from '@gisatcz/ptr-be-core/browser'; const CogBitmapLayer = geolib.CogBitmapLayer; /** + * @deprecated Used by RenderingMap which is deprecated and will be removed in future versions. Use MapSet and related components instead. * Creates a COG (Cloud Optimized GeoTIFF) layer for rendering raster data on a map using @gisatcz/deckgl-geolib library * * @param {LayerGeneralProps} props - The properties required to create the COG layer. diff --git a/src/client/map/logic/map.layers.geojson.ts b/src/client/map/logic/map.layers.geojson.ts index 54f1707..ae02df7 100644 --- a/src/client/map/logic/map.layers.geojson.ts +++ b/src/client/map/logic/map.layers.geojson.ts @@ -5,9 +5,6 @@ import { UsedDatasourceLabels } from '@gisatcz/ptr-be-core/browser'; import { getFeatureId } from '../../shared/helpers/getFeatureId'; import { hexToRgbArray } from '../../shared/helpers/hexToRgbArray'; import { SELECTION_DEFAULT_COLOUR } from '../../shared/constants/colors'; -import { useAxios } from '../../shared/hooks/useAxios'; - -const defaultRoute = '/api/data/geojson'; /** * Represents the structure needed for feature identification and property access. @@ -33,6 +30,7 @@ const defaultLayerStyle = { }; /** + * @deprecated Used by RenderingMap which is deprecated and will be removed in future versions. Use MapSet and related components instead. * Creates a GeoJsonLayer with the specified properties. * * @param {LayerGeneralProps} param0 - The properties for creating the GeoJsonLayer. @@ -41,13 +39,11 @@ const defaultLayerStyle = { * @param {string} param0.key - Layer identifier. * @param {number} param0.opacity - Layer opacity. * @param {Object} param0.selection - Selection object containing featureKeys, distinctColours, and featureKeyColourIndexPairs. - * @param {string} [param0.route=defaultRoute] - Custom route for fetching GeoJSON data. * @returns {GeoJsonLayer} The created GeoJsonLayer instance. * * @todo This is only a first draft of the GeoJSON layer implementation. * TODO: Add support for fill styling, point sizes, and other styling options. * TODO: featureIdProperty should be defined and validated in the datasource configuration, not just in geojsonOptions. - * TODO: Delete "xxx" check after url becames optional for geojson. */ export const createGeojsonLayer = ({ sourceNode, @@ -56,40 +52,8 @@ export const createGeojsonLayer = ({ key, opacity, selection, - route = defaultRoute, }: LayerGeneralProps) => { - const { documentId, validIntervalIso } = sourceNode; - const { url, configurationJs } = validateDatasource(sourceNode, UsedDatasourceLabels.Geojson, false); - - const axiosResult = useAxios({ fetchUrl: route }, undefined, { documentId, validIntervalIso }, { method: 'POST' }); - - let data: unknown; - let error: unknown; - let loading: boolean = false; - - /** - * The "xxx" value is currently used as a placeholder to indicate that the URL is not available or should not be used. - * Once the url property is truly optional for geojson nodes, remove this workaround and handle data loading more robustly. - */ - if (url && url !== 'xxx') { - data = url; - error = undefined; - loading = false; - } else { - data = axiosResult.data; - error = axiosResult.error; - loading = axiosResult.isLoading; - } - - // While data is loading, do not render the layer - if (loading) { - return null; - } - - // Log an error if data fetching fails - if (error) { - console.error('Error loading map data:', error); - } + const { url, configurationJs } = validateDatasource(sourceNode, UsedDatasourceLabels.Geojson, true); const selectedFeatureKeys = selection?.featureKeys ?? []; const distinctColours = selection?.distinctColours ?? [SELECTION_DEFAULT_COLOUR]; @@ -135,7 +99,7 @@ export const createGeojsonLayer = ({ id: key, opacity: opacity ?? 1, visible: isActive, - data, + data: url, updateTriggers: { getLineColor: [layerStyle, selection], getFillColor: [layerStyle, selection], diff --git a/src/client/map/logic/map.layers.mvt.ts b/src/client/map/logic/map.layers.mvt.ts index 76261a7..4d15b7b 100644 --- a/src/client/map/logic/map.layers.mvt.ts +++ b/src/client/map/logic/map.layers.mvt.ts @@ -4,6 +4,7 @@ import { validateDatasource } from './validate.layers'; import { UsedDatasourceLabels } from '@gisatcz/ptr-be-core/browser'; /** + * @deprecated Used by RenderingMap which is deprecated and will be removed in future versions. Use MapSet and related components instead. * Creates an MVT (Mapbox Vector Tile) layer using DeckGL. * * @param {LayerGeneralProps} param0 - The properties for creating the MVT layer. diff --git a/src/client/map/logic/map.layers.tile.ts b/src/client/map/logic/map.layers.tile.ts index a87a39b..21b6c73 100644 --- a/src/client/map/logic/map.layers.tile.ts +++ b/src/client/map/logic/map.layers.tile.ts @@ -5,6 +5,7 @@ import { validateDatasource } from './validate.layers'; import { UsedDatasourceLabels } from '@gisatcz/ptr-be-core/browser'; /** + * @deprecated Used by RenderingMap which is deprecated and will be removed in future versions. Use MapSet and related components instead. * Creates a TileLayer for DeckGL using the provided source node and activity status. * * @param {LayerGeneralProps} param0 - The properties for the layer. diff --git a/src/client/map/logic/map.layers.wms.ts b/src/client/map/logic/map.layers.wms.ts index b5aaeb2..015435e 100644 --- a/src/client/map/logic/map.layers.wms.ts +++ b/src/client/map/logic/map.layers.wms.ts @@ -4,6 +4,7 @@ import { validateDatasource } from './validate.layers'; import { UsedDatasourceLabels } from '@gisatcz/ptr-be-core/browser'; /** + * @deprecated Used by RenderingMap which is deprecated and will be removed in future versions. Use MapSet and related components instead. * Creates a WMS (Web Map Service) layer using the provided source node and activation status. * * @param {LayerGeneralProps} param0 - The properties for creating the WMS layer. diff --git a/src/client/map/logic/parsing.layers.ts b/src/client/map/logic/parsing.layers.ts index 82fed13..f0f0c09 100644 --- a/src/client/map/logic/parsing.layers.ts +++ b/src/client/map/logic/parsing.layers.ts @@ -19,6 +19,7 @@ export interface ParseLayersProps { } /** + * @deprecated Used by RenderingMap which is deprecated and will be removed in future versions. * Parses rendering layers from shared state and returns an array of DeckGL layers. * Uses a selector callback to retrieve selection objects for each layer. * @@ -47,7 +48,7 @@ export const parseLayersFromSharedState = ({ isInteractive: layer.isInteractive, onClickHandler: layer.interaction && interactionRenderingMap?.get(layer.interaction), selection: selectionForLayer, - route: layer.route, + route: layer.fetchOptions?.route, }; // TODO: add other layer types and datasources diff --git a/src/client/map/logic/validate.layers.ts b/src/client/map/logic/validate.layers.ts index a764016..7ed8c81 100644 --- a/src/client/map/logic/validate.layers.ts +++ b/src/client/map/logic/validate.layers.ts @@ -2,6 +2,7 @@ import { UsedDatasourceLabels } from '@gisatcz/ptr-be-core/browser'; import { DatasourceWithNeighbours } from '../../shared/models/models.metadata'; /** + * @deprecated Used by RenderingMap which is deprecated and will be removed in future versions. * Validates datasource node for required fields * @param source Data source node * @param requiredDatasourceType Expected datasource type diff --git a/src/client/shared/hooks/useAxios.ts b/src/client/shared/hooks/useAxios.ts index 6c1d2b9..11abe67 100644 --- a/src/client/shared/hooks/useAxios.ts +++ b/src/client/shared/hooks/useAxios.ts @@ -3,24 +3,14 @@ import axios, { AxiosRequestConfig } from 'axios'; /** * Options for configuring the `useAxios` hook. - * @interface - * @property {AxiosRequestConfig} [axiosConfig] - Optional Axios configuration object. - * @property {'GET' | 'POST'} [method] - HTTP method to use for the request (default is 'GET'). */ export interface UseAxiosOptions { axiosConfig?: AxiosRequestConfig; method?: 'GET' | 'POST'; + /** If true, the request will not be executed. */ + skip?: boolean; } -/** - * Return type of the `useAxios` hook. - * @interface - * @template T - * @property {T | null} data - The response data from the Axios request, or null if no data is available. - * @property {any | null} error - The error object if the request fails, or null if no error occurred. - * @property {boolean} isLoading - Indicates whether the request is currently loading. - * @property {boolean} isValidating - Indicates whether the request is currently being validated. - */ export interface UseAxiosReturn { data: T | null; error: any | null; @@ -30,24 +20,10 @@ export interface UseAxiosReturn { /** * A custom React hook for making Axios HTTP requests. - * - * @template T - * @param {{ fetchUrl: string }} url - The URL configuration object containing the endpoint to fetch. - * @param fetcher - Optional custom fetcher function for GET requests. - * @param {any} [payload] - The payload to send with the request (used for POST requests). - * @param {UseAxiosOptions} [options] - Optional configuration for the Axios request. - * @returns {UseAxiosReturn} - An object containing the response data, error, loading state, and validation state. - * - * @example - * const { data, error, isLoading, isValidating } = useAxios( - * { fetchUrl: '/api/data' }, - * undefined, - * { method: 'POST', axiosConfig: { headers: { 'Content-Type': 'application/json' } } }, - * { key: 'value' } - * ); + * Now supports a `skip` option to conditionally prevent execution. */ export function useAxios( - url: { fetchUrl: string }, + url: { fetchUrl: string | undefined | null }, fetcher?: (url: string) => Promise, payload?: unknown, options: UseAxiosOptions = {} @@ -57,22 +33,36 @@ export function useAxios( const [isLoading, setIsLoading] = useState(false); const [isValidating, setIsValidating] = useState(false); + // Extract variables for the dependency array to prevent unnecessary re-runs + const { skip, method: optMethod, axiosConfig } = options; + const fetchUrl = url.fetchUrl; + useEffect(() => { - (async () => { + // 1. If skip is true or fetchUrl is missing, reset state and do nothing + if (skip || !fetchUrl) { + setIsLoading(false); + setIsValidating(false); + return; + } + + const fetchData = async () => { setIsValidating(true); setIsLoading(true); setError(null); + try { - const method = (options.method ?? 'GET').toUpperCase() as 'GET' | 'POST'; + const method = (optMethod ?? 'GET').toUpperCase() as 'GET' | 'POST'; let responseData: T; + if (method === 'GET') { if (fetcher) { - responseData = await fetcher(url.fetchUrl); + responseData = await fetcher(fetchUrl); } else { - responseData = (await axios.get(url.fetchUrl, options.axiosConfig)).data; + responseData = (await axios.get(fetchUrl, axiosConfig)).data; } } else { - responseData = (await axios.post(url.fetchUrl, payload, options.axiosConfig)).data; + // POST request + responseData = (await axios.post(fetchUrl, payload, axiosConfig)).data; } setData(responseData); @@ -82,8 +72,10 @@ export function useAxios( setIsValidating(false); setIsLoading(false); } - })(); - }, [url.fetchUrl, fetcher, JSON.stringify(payload), options.method, JSON.stringify(options.axiosConfig)]); + }; + + fetchData(); + }, [fetchUrl, fetcher, JSON.stringify(payload), optMethod, JSON.stringify(axiosConfig), skip]); return { data, error, isLoading, isValidating }; } diff --git a/src/client/shared/models/models.layers.ts b/src/client/shared/models/models.layers.ts index 0791f0b..75cea57 100644 --- a/src/client/shared/models/models.layers.ts +++ b/src/client/shared/models/models.layers.ts @@ -7,12 +7,15 @@ import { DatasourceWithNeighbours } from './models.metadata'; */ export interface RenderingLayer { isActive: boolean; - level: number; + level?: number; key: string; opacity?: number; datasource: DatasourceWithNeighbours; interaction: Nullable; selectionKey?: string; isInteractive?: boolean; - route?: string; + fetchOptions?: { + route: string; + method: 'GET' | 'POST'; + }; } diff --git a/src/client/shared/models/models.tooltip.ts b/src/client/shared/models/models.tooltip.ts new file mode 100644 index 0000000..17fc30e --- /dev/null +++ b/src/client/shared/models/models.tooltip.ts @@ -0,0 +1,10 @@ +/** + * Defines the shape of a single attribute to be displayed in a map tooltip. + */ +export interface TooltipAttribute { + key: string; + label?: string; + value?: string | number; + unit?: string; + decimalPlaces?: number; +} diff --git a/src/client/story/utils/getTooltipAttributes.ts b/src/client/story/utils/getTooltipAttributes.ts index a5cde4c..3940f52 100644 --- a/src/client/story/utils/getTooltipAttributes.ts +++ b/src/client/story/utils/getTooltipAttributes.ts @@ -1,18 +1,4 @@ -/** - * Tooltip attribute definition. - * @property {string} key - Unique key for the attribute. - * @property {string} [label] - Optional label for display. - * @property {string|number} [value] - Value to display. - * @property {string} [unit] - Optional unit for the value. - * @property {number} [decimalPlaces] - Optional decimal places for number formatting. - */ -export interface TooltipAttribute { - key: string; - label?: string; - value?: string | number; - unit?: string; - decimalPlaces?: number; -} +import { TooltipAttribute } from '../../shared/models/models.tooltip'; /** * Maps feature properties to tooltip attributes based on settings.