diff --git a/packages/base/src/dialogs/symbology/hooks/useGetBandInfo.ts b/packages/base/src/dialogs/symbology/hooks/useGetBandInfo.ts new file mode 100644 index 000000000..86f884f60 --- /dev/null +++ b/packages/base/src/dialogs/symbology/hooks/useGetBandInfo.ts @@ -0,0 +1,101 @@ +import { IDict, IJGISLayer, IJupyterGISModel } from '@jupytergis/schema'; +import { DocumentRegistry } from '@jupyterlab/docregistry'; +import { useEffect, useState } from 'react'; +import { loadGeoTIFFWithCache } from '../../../tools'; + +export interface IBandHistogram { + buckets: number[]; + count: number; + max: number; + min: number; +} + +export interface IBandRow { + band: number; + colorInterpretation: string; + stats: { + minimum: number; + maximum: number; + mean: number; + stdDev: number; + }; + metadata: IDict; + histogram: IBandHistogram; +} + +interface ITifBandData { + band: number; + colorInterpretation: string; + minimum: number; + maximum: number; + mean: number; + stdDev: number; + metadata: object; + histogram: any; +} + +const preloadGeoTiffFile = async (sourceInfo: { url?: string | undefined }) => { + return await loadGeoTIFFWithCache(sourceInfo); +}; + +const useGetBandInfo = ( + context: DocumentRegistry.IContext, + layer: IJGISLayer +) => { + const [bandRows, setBandRows] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchBandInfo = async () => { + setLoading(true); + setError(null); + + try { + const bandsArr: IBandRow[] = []; + const source = context.model.getSource(layer?.parameters?.source); + const sourceInfo = source?.parameters?.urls[0]; + + if (!sourceInfo?.url) { + setError('No source URL found.'); + setLoading(false); + return; + } + + const preloadedFile = await preloadGeoTiffFile(sourceInfo); + const { file, metadata, sourceUrl } = { ...preloadedFile }; + + if (file && metadata && sourceUrl === sourceInfo.url) { + metadata['bands'].forEach((bandData: ITifBandData) => { + bandsArr.push({ + band: bandData.band, + colorInterpretation: bandData.colorInterpretation, + stats: { + minimum: sourceInfo.min ?? bandData.minimum, + maximum: sourceInfo.max ?? bandData.maximum, + mean: bandData.mean, + stdDev: bandData.stdDev + }, + metadata: bandData.metadata, + histogram: bandData.histogram + }); + }); + + setBandRows(bandsArr); + } else { + setError('Failed to preload the file or metadata mismatch.'); + } + } catch (err: any) { + setError(`Error fetching band info: ${err.message}`); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchBandInfo(); + }, []); + + return { bandRows, setBandRows, loading, error }; +}; + +export default useGetBandInfo; diff --git a/packages/base/src/dialogs/symbology/tiff_layer/TiffRendering.tsx b/packages/base/src/dialogs/symbology/tiff_layer/TiffRendering.tsx index 9b3be7cf1..9e923a5a5 100644 --- a/packages/base/src/dialogs/symbology/tiff_layer/TiffRendering.tsx +++ b/packages/base/src/dialogs/symbology/tiff_layer/TiffRendering.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; import { ISymbologyDialogProps } from '../symbologyDialog'; import SingleBandPseudoColor from './types/SingleBandPseudoColor'; +import MultibandColor from './types/MultibandColor'; const TiffRendering = ({ context, @@ -10,13 +11,20 @@ const TiffRendering = ({ layerId }: ISymbologyDialogProps) => { const renderTypes = ['Singleband Pseudocolor', 'Multiband Color']; - const [selectedRenderType, setSelectedRenderType] = useState( - 'Singleband Pseudocolor' - ); + const [selectedRenderType, setSelectedRenderType] = useState(); const [componentToRender, setComponentToRender] = useState(null); let RenderComponent; + if (!layerId) { + return; + } + useEffect(() => { + const layer = context.model.getLayer(layerId); + const renderType = layer?.parameters?.symbologyState.renderType; + setSelectedRenderType(renderType ?? 'Singleband Pseudocolor'); + }, []); + useEffect(() => { if (!selectedRenderType) { return; @@ -34,6 +42,17 @@ const TiffRendering = ({ /> ); break; + case 'Multiband Color': + RenderComponent = ( + + ); + break; default: RenderComponent =
Render Type Not Implemented (yet)
; } diff --git a/packages/base/src/dialogs/symbology/tiff_layer/components/BandRow.tsx b/packages/base/src/dialogs/symbology/tiff_layer/components/BandRow.tsx index 50aeef8b6..cc9666861 100644 --- a/packages/base/src/dialogs/symbology/tiff_layer/components/BandRow.tsx +++ b/packages/base/src/dialogs/symbology/tiff_layer/components/BandRow.tsx @@ -1,21 +1,37 @@ import React, { useState } from 'react'; -import { IBandRow } from '../types/SingleBandPseudoColor'; +import { IBandRow } from '../../hooks/useGetBandInfo'; -const BandRow = ({ - index, - bandRow, - bandRows, - setSelectedBand, - setBandRows -}: { +interface IBandRowProps { + label: string; index: number; bandRow: IBandRow; bandRows: IBandRow[]; setSelectedBand: (band: number) => void; setBandRows: (bandRows: IBandRow[]) => void; -}) => { - const [minValue, setMinValue] = useState(bandRow.stats.minimum); - const [maxValue, setMaxValue] = useState(bandRow.stats.maximum); + isMultibandColor?: boolean; +} + +/** + * + * @param label Label displayed in symbology dialog + * @param index Index of current row in band row data + * @param bandRow Band from bands array, will be undefined when band is 'unset' in Multiband color + * @param bandRows Bands array from tiff data + * @param setSelectedBand Function to set selected band parent + * @param setBandRows Function to update band rows in parent + * @param isMultibandColor Used to hide min/max input and add 'Unset' option to drop down menu for MultiBand symbology + */ +const BandRow = ({ + label, + index, + bandRow, + bandRows, + setSelectedBand, + setBandRows, + isMultibandColor +}: IBandRowProps) => { + const [minValue, setMinValue] = useState(bandRow?.stats.minimum); + const [maxValue, setMaxValue] = useState(bandRow?.stats.maximum); const handleMinValueChange = (event: { target: { value: string | number }; @@ -41,7 +57,7 @@ const BandRow = ({ return ( <>
- +
-
-
- - -
-
- - + {isMultibandColor ? null : ( +
+
+ + +
+
+ + +
-
+ )} ); }; diff --git a/packages/base/src/dialogs/symbology/tiff_layer/types/MultibandColor.tsx b/packages/base/src/dialogs/symbology/tiff_layer/types/MultibandColor.tsx new file mode 100644 index 000000000..0353a87de --- /dev/null +++ b/packages/base/src/dialogs/symbology/tiff_layer/types/MultibandColor.tsx @@ -0,0 +1,159 @@ +import { IWebGlLayer } from '@jupytergis/schema'; +import { ExpressionValue } from 'ol/expr/expression'; +import React, { useEffect, useRef, useState } from 'react'; +import { Spinner } from '../../../../mainview/spinner'; +import useGetBandInfo from '../../hooks/useGetBandInfo'; +import { ISymbologyDialogProps } from '../../symbologyDialog'; +import BandRow from '../components/BandRow'; + +interface ISelectedBands { + red: number; + green: number; + blue: number; +} + +type rgbEnum = 'red' | 'green' | 'blue'; + +const MultibandColor = ({ + context, + okSignalPromise, + cancel, + layerId +}: ISymbologyDialogProps) => { + if (!layerId) { + return; + } + const layer = context.model.getLayer(layerId); + if (!layer?.parameters) { + return; + } + + const { bandRows, setBandRows, loading } = useGetBandInfo(context, layer); + + const [selectedBands, setSelectedBands] = useState({ + red: 1, + green: 2, + blue: 3 + }); + + const numOfBandsRef = useRef(0); + const selectedBandsRef = useRef({ + red: selectedBands.red, + green: selectedBands.green, + blue: selectedBands.blue + }); + + useEffect(() => { + populateOptions(); + + okSignalPromise.promise.then(okSignal => { + okSignal.connect(handleOk); + }); + + return () => { + okSignalPromise.promise.then(okSignal => { + okSignal.disconnect(handleOk, this); + }); + }; + }, []); + + useEffect(() => { + numOfBandsRef.current = bandRows.length; + }, [bandRows]); + + useEffect(() => { + selectedBandsRef.current = selectedBands; + }, [selectedBands]); + + const populateOptions = async () => { + const layerParams = layer.parameters as IWebGlLayer; + const red = layerParams.symbologyState?.redBand ?? 1; + const green = layerParams.symbologyState?.greenBand ?? 2; + const blue = layerParams.symbologyState?.blueBand ?? 3; + + setSelectedBands({ red, green, blue }); + }; + + const updateBand = (color: rgbEnum, value: number) => { + setSelectedBands(prevBands => ({ + ...prevBands, + [color]: value + })); + }; + + const handleOk = () => { + // Update layer + if (!layer.parameters) { + return; + } + + const colorExpr: ExpressionValue[] = ['array']; + const rgb: rgbEnum[] = ['red', 'green', 'blue']; + + rgb.forEach(color => { + const bandValue = selectedBandsRef.current[color]; + colorExpr.push(bandValue !== 0 ? ['band', bandValue] : 0); + }); + + // Array expression expects 4 values + // Last band should be alpha band added by OpenLayers + colorExpr.push(['band', numOfBandsRef.current + 1]); + + const symbologyState = { + renderType: 'Multiband Color', + redBand: selectedBandsRef.current['red'], + greenBand: selectedBandsRef.current['green'], + blueBand: selectedBandsRef.current['blue'] + }; + + layer.parameters.symbologyState = symbologyState; + layer.parameters.color = colorExpr; + + context.model.sharedModel.updateLayer(layerId, layer); + cancel(); + }; + + return ( +
+
+ {loading ? ( + + ) : ( + <> + updateBand('red', val)} + setBandRows={setBandRows} + isMultibandColor={true} + /> + + updateBand('green', val)} + setBandRows={setBandRows} + isMultibandColor={true} + /> + + updateBand('blue', val)} + setBandRows={setBandRows} + isMultibandColor={true} + /> + + )} +
+
+ ); +}; + +export default MultibandColor; diff --git a/packages/base/src/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.tsx b/packages/base/src/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.tsx index 1956017eb..23e8ceec3 100644 --- a/packages/base/src/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.tsx +++ b/packages/base/src/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.tsx @@ -1,86 +1,57 @@ -import { IDict, IWebGlLayer } from '@jupytergis/schema'; +import { IWebGlLayer } from '@jupytergis/schema'; import { Button } from '@jupyterlab/ui-components'; import { ReadonlyJSONObject } from '@lumino/coreutils'; import { ExpressionValue } from 'ol/expr/expression'; import React, { useEffect, useRef, useState } from 'react'; -import { GeoTiffClassifications } from '../../classificationModes'; +import { Spinner } from '../../../../mainview/spinner'; import { GlobalStateDbManager } from '../../../../store'; -import { IStopRow, ISymbologyDialogProps } from '../../symbologyDialog'; -import BandRow from '../components/BandRow'; +import { GeoTiffClassifications } from '../../classificationModes'; import ColorRamp, { ColorRampOptions } from '../../components/color_ramp/ColorRamp'; import StopRow from '../../components/color_stops/StopRow'; +import useGetBandInfo, { IBandRow } from '../../hooks/useGetBandInfo'; +import { IStopRow, ISymbologyDialogProps } from '../../symbologyDialog'; import { Utils } from '../../symbologyUtils'; -import { Spinner } from '../../../../mainview/spinner'; -import { loadGeoTIFFWithCache } from '../../../../tools'; - -export interface IBandRow { - band: number; - colorInterpretation: string; - stats: { - minimum: number; - maximum: number; - mean: number; - stdDev: number; - }; - metadata: IDict; - histogram: IBandHistogram; -} - -export interface IBandHistogram { - buckets: number[]; - count: number; - max: number; - min: number; -} +import BandRow from '../components/BandRow'; export type InterpolationType = 'discrete' | 'linear' | 'exact'; -type TifBandData = { - band: number; - colorInterpretation: string; - minimum: number; - maximum: number; - mean: number; - stdDev: number; - metadata: any; - histogram: IBandHistogram; -}; - const SingleBandPseudoColor = ({ context, okSignalPromise, cancel, layerId }: ISymbologyDialogProps) => { + if (!layerId) { + return; + } + const layer = context.model.getLayer(layerId); + if (!layer?.parameters) { + return; + } + const functions = ['discrete', 'linear', 'exact']; const modeOptions = ['continuous', 'equal interval', 'quantile']; - const stopRowsRef = useRef(); - const bandRowsRef = useRef([]); - const selectedFunctionRef = useRef(); - const colorRampOptionsRef = useRef(); - const layerStateRef = useRef(); - const selectedBandRef = useRef(); + + const stateDb = GlobalStateDbManager.getInstance().getStateDb(); + + const { bandRows, setBandRows, loading } = useGetBandInfo(context, layer); const [layerState, setLayerState] = useState(); const [selectedBand, setSelectedBand] = useState(1); const [stopRows, setStopRows] = useState([]); - const [bandRows, setBandRows] = useState([]); const [selectedFunction, setSelectedFunction] = useState('linear'); const [colorRampOptions, setColorRampOptions] = useState< ColorRampOptions | undefined >(); - if (!layerId) { - return; - } - const layer = context.model.getLayer(layerId); - if (!layer?.parameters) { - return; - } - const stateDb = GlobalStateDbManager.getInstance().getStateDb(); + const stopRowsRef = useRef(); + const bandRowsRef = useRef([]); + const selectedFunctionRef = useRef(); + const colorRampOptionsRef = useRef(); + const selectedBandRef = useRef(); useEffect(() => { populateOptions(); @@ -96,11 +67,6 @@ const SingleBandPseudoColor = ({ }; }, []); - useEffect(() => { - layerStateRef.current = layerState; - getBandInfo(); - }, [layerState]); - useEffect(() => { bandRowsRef.current = bandRows; buildColorInfo(); @@ -111,7 +77,6 @@ const SingleBandPseudoColor = ({ selectedFunctionRef.current = selectedFunction; colorRampOptionsRef.current = colorRampOptions; selectedBandRef.current = selectedBand; - layerStateRef.current = layerState; }, [stopRows, selectedFunction, colorRampOptions, selectedBand, layerState]); const populateOptions = async () => { @@ -129,44 +94,6 @@ const SingleBandPseudoColor = ({ setSelectedFunction(interpolation); }; - const preloadGeoTiffFile = async (sourceInfo: { - url?: string | undefined; - }) => { - return await loadGeoTIFFWithCache(sourceInfo); - }; - - const getBandInfo = async () => { - const bandsArr: IBandRow[] = []; - const source = context.model.getSource(layer?.parameters?.source); - const sourceInfo = source?.parameters?.urls[0]; - - if (!sourceInfo?.url) { - return; - } - - // Preload the file only once - const preloadedFile = await preloadGeoTiffFile(sourceInfo); - const { file, metadata, sourceUrl } = { ...preloadedFile }; - - if (file && metadata && sourceUrl === sourceInfo.url) { - metadata['bands'].forEach((bandData: TifBandData) => { - bandsArr.push({ - band: bandData.band, - colorInterpretation: bandData.colorInterpretation, - stats: { - minimum: sourceInfo.min ?? bandData.minimum, - maximum: sourceInfo.max ?? bandData.maximum, - mean: bandData.mean, - stdDev: bandData.stdDev - }, - metadata: bandData.metadata, - histogram: bandData.histogram - }); - }); - setBandRows(bandsArr); - } - }; - const buildColorInfo = () => { // This it to parse a color object on the layer if (!layer.parameters?.color || !layerState) { @@ -433,10 +360,11 @@ const SingleBandPseudoColor = ({ return (
- {bandRows.length === 0 ? ( - + {loading ? ( + ) : (