Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/client/map/MapSet/MapTooltip/MapTooltip.tsx
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
3 changes: 2 additions & 1 deletion src/client/map/MapSet/MapTooltip/getMapTooltip.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down
61 changes: 31 additions & 30 deletions src/client/map/MapSet/SingleMap.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -29,6 +29,8 @@ export interface BasicMapProps {
CustomTooltip?: React.ElementType | boolean;
}

type LayerRegistry = Record<string, LayerInstance>;

/**
* SingleMap component intended to be used in MapSet component.
*
Expand All @@ -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<LayerRegistry>({});

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.
*/
Expand Down Expand Up @@ -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).
*
Expand All @@ -153,9 +153,10 @@ export const SingleMap = ({ mapKey, syncedView, CustomTooltip = false }: BasicMa

return (
<>
<LayerManager layers={mapLayers} onLayerUpdate={handleLayerUpdate} />
<DeckGL
viewState={mapViewState}
layers={layers}
layers={activeLayers}
controller={true}
width="100%"
height="100%"
Expand Down
3 changes: 2 additions & 1 deletion src/client/map/MapSet/handleMapHover.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
* Tooltip state object.
Expand Down
4 changes: 3 additions & 1 deletion src/client/map/components/RenderingMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ export interface RenderMapProps {
setDistanceScales?: (distanceScales: { unitsPerDegree?: Array<number>; metersPerUnit: Array<number> }) => 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<RenderMapProps> = (props: RenderMapProps) => {
// shared application state in context
const [sharedState] = useSharedState();
Expand Down
75 changes: 75 additions & 0 deletions src/client/map/components/layers/COGLayerSource.tsx
Original file line number Diff line number Diff line change
@@ -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)]);
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using JSON.stringify(cogBitmapOptions) in the dependency array causes the useMemo to recompute on every render when cogBitmapOptions is an object, defeating its purpose. Consider using a more stable approach such as storing the stringified value in a separate useMemo, or redesigning the data flow to avoid object reference changes.

Copilot uses AI. Check for mistakes.

/**
* 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;
});
Loading