Skip to content

Commit

Permalink
[E & A] Implement colormap configurability (#1117)
Browse files Browse the repository at this point in the history
**Related Ticket:** #994

### Description of Changes

Implements the color map configuration of E&A layers.

Changes:

- Adds curated list of supported color maps taken from the back-end,
split into diverging and sequential groups
- Supports other valid color map names from the list of color maps:
https://openveda.cloud/api/raster/colorMaps
- Adds support for reversing the colors
- Adds reset all button which currently only resets the "Reverse"
option, but will also apply to the "Rescale" functionality which is to
be added next
- Added a loading skeleton to the colormap in the timeline sidebar when
a layer is added. This prevents a visual flash because it might take a
second to fetch the color map from the back-end (cc @faustoperez )

The precedence of the color maps are as follows:

1. Start with what is configured in the sourceParams of the dataset
2. If it's not defined, check for the dashboard render configuration
coming from STAC endpoint
3. If still undefined, check for asset-specific renders using the
sourceParams' assets. This is a special edge case for some datasets that
have multiple assets (multiple assets under one collection item)
4. If all else fails, default to `viridis`

Please let me know your thoughts on that or if you spot something
strange

### Notes & Questions About Changes

- I additionally pushed a shell script that I used to build the
`colorMaps.ts` file, I hope that's okay. It fetches all the `rgba`
values for each supported colorMap from titiler. We could extend it and
integrate it within our Github workflow in the future, so that the
colors are fetched on each new release (although it's unlikely they will
change that often)
 
### Validation / Testing

- Verify that the color map list is correctly split into diverging and
sequential groups
- Check that reversing the color map works as expected and toggles
correctly
- Test the "Reset All" button to ensure that it resets the "Reverse"
option and works as intended
- Check that the precedence of the color maps is followed in the correct
order:

1. Start with sourceParams of the dataset
2. Fallback to dashboard render configuration from the STAC endpoint
3. If undefined, check for asset-specific renders using sourceParams'
assets
4. Finally, default to `viridis` if all else fails

- Verify that the changes do not break any existing functionality
related to rendering of the tiles on the map (selected color map must
match the tile colors)
- Test across different datasets with both single and multiple assets to
confirm edge cases are handled as described
  • Loading branch information
dzole0311 authored Aug 29, 2024
2 parents ebfa3ac + 60559fd commit f0a4ace
Show file tree
Hide file tree
Showing 24 changed files with 786 additions and 253 deletions.
51 changes: 46 additions & 5 deletions app/scripts/components/common/map/layer-legend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
WidgetItemHGroup
} from '$styles/panel';
import { LayerLegendCategorical, LayerLegendGradient } from '$types/veda';
import { divergingColorMaps, sequentialColorMaps } from '$components/exploration/components/datasets/colorMaps';

interface LayerLegendCommonProps {
id: string;
Expand Down Expand Up @@ -300,18 +301,22 @@ export function LayerCategoricalGraphic(props: LayerLegendCategorical) {
);
}

export function LayerGradientGraphic(props: LayerLegendGradient) {
export const LayerGradientGraphic = (props: LayerLegendGradient) => {
const { stops, min, max, unit } = props;

const [hoverVal, setHoverVal] = useState(0);

const moveListener = useCallback(
(e) => {
const width = e.nativeEvent.target.clientWidth;
const target = e.nativeEvent.target;
const boundingRect = target.getBoundingClientRect();
const offsetX = e.nativeEvent.clientX - boundingRect.left;
const width = boundingRect.width;

const scale = scaleLinear()
.domain([0, width])
.range([Number(min), Number(max)]);
setHoverVal(scale(e.nativeEvent.layerX));

setHoverVal(Math.max(Number(min), Math.min(Number(max), scale(offsetX))));
},
[min, max]
);
Expand Down Expand Up @@ -340,4 +345,40 @@ export function LayerGradientGraphic(props: LayerLegendGradient) {
{unit?.label && <dd className='unit'>{unit.label}</dd>}
</LegendList>
);
}
};

export const LayerGradientColormapGraphic = (props: Omit<LayerLegendGradient, 'stops' | 'type'>) => {
const { colorMap, ...otherProps } = props;

const colormap = findColormapByName(colorMap ?? 'viridis');
if (!colormap) {
return null;
}

const stops = Object.values(colormap).map((value) => {
if (Array.isArray(value) && value.length === 4) {
return `rgba(${value.join(',')})`;
} else {
return `rgba(0, 0, 0, 1)`;
}
});

const processedStops = colormap.isReversed
? stops.reduceRight((acc, stop) => [...acc, stop], [])
: stops;

return <LayerGradientGraphic type='gradient' stops={processedStops} {...otherProps} />;
};

export const findColormapByName = (name: string) => {
const isReversed = name.toLowerCase().endsWith('_r');
const baseName = isReversed ? name.slice(0, -2).toLowerCase() : name.toLowerCase();

const colormap = sequentialColorMaps[baseName] ?? divergingColorMaps[baseName];

if (!colormap) {
return null;
}

return { ...colormap, isReversed };
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface RasterPaintLayerProps extends BaseGeneratorParams {
tileApiEndpoint?: string;
zoomExtent?: number[];
assetUrl: string;
colorMap?: string | undefined;
}

export function RasterPaintLayer(props: RasterPaintLayerProps) {
Expand All @@ -24,19 +25,24 @@ export function RasterPaintLayer(props: RasterPaintLayerProps) {
zoomExtent,
assetUrl,
hidden,
opacity
opacity,
colorMap
} = props;

const { updateStyle } = useMapStyle();
const [minZoom] = zoomExtent ?? [0, 20];
const generatorId = `zarr-timeseries-${id}`;

const updatedSourceParams = useMemo(() => {
return { ...sourceParams, ...colorMap && {colormap_name: colorMap}};
}, [sourceParams, colorMap]);

//
// Generate Mapbox GL layers and sources for raster timeseries
//
const haveSourceParamsChanged = useMemo(
() => JSON.stringify(sourceParams),
[sourceParams]
() => JSON.stringify(updatedSourceParams),
[updatedSourceParams]
);

const generatorParams = useGeneratorParams(props);
Expand All @@ -48,7 +54,7 @@ export function RasterPaintLayer(props: RasterPaintLayerProps) {
const tileParams = qs.stringify({
url: assetUrl,
time_slice: date,
...sourceParams
...updatedSourceParams,
});

const zarrSource: RasterSource = {
Expand All @@ -65,12 +71,13 @@ export function RasterPaintLayer(props: RasterPaintLayerProps) {
paint: {
'raster-opacity': hidden ? 0 : rasterOpacity,
'raster-opacity-transition': {
duration: 320
}
duration: 320,
},
},
minzoom: minZoom,
metadata: {
layerOrderPosition: 'raster'
layerOrderPosition: 'raster',
colorMapVersion: colorMap,
}
};

Expand All @@ -96,7 +103,8 @@ export function RasterPaintLayer(props: RasterPaintLayerProps) {
minZoom,
tileApiEndpoint,
haveSourceParamsChanged,
generatorParams
generatorParams,
colorMap
// generatorParams includes hidden and opacity
// hidden,
// opacity,
Expand All @@ -119,4 +127,4 @@ export function RasterPaintLayer(props: RasterPaintLayerProps) {
}, [updateStyle, generatorId]);

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ export function RasterTimeseries(props: RasterTimeseriesProps) {
hidden,
opacity,
stacApiEndpoint,
tileApiEndpoint
tileApiEndpoint,
colorMap,
} = props;

const { current: mapInstance } = useMaps();
Expand Down Expand Up @@ -259,7 +260,7 @@ export function RasterTimeseries(props: RasterTimeseriesProps) {
LOG && console.log('Payload', payload);
LOG && console.groupEnd();
/* eslint-enable no-console */

let responseData;

try {
Expand All @@ -271,19 +272,19 @@ export function RasterTimeseries(props: RasterTimeseriesProps) {
const mosaicUrl = responseData.links[1].href;
setMosaicUrl(mosaicUrl.replace('/{tileMatrixSetId}', '/WebMercatorQuad'));
} catch (error) {
// @NOTE: conditional logic TO BE REMOVED once new BE endpoints have moved to prod... Fallback on old request url if new endpoints error with nonexistance...
// @NOTE: conditional logic TO BE REMOVED once new BE endpoints have moved to prod... Fallback on old request url if new endpoints error with nonexistance...
if (error.request) {
// The request was made but no response was received
responseData = await requestQuickCache<any>({
url: `${tileApiEndpointToUse}/mosaic/register`, // @NOTE: This will fail anyways with "staging-raster.delta-backend.com" because its already deprecated...
payload,
controller
});

const mosaicUrl = responseData.links[1].href;
setMosaicUrl(mosaicUrl);
} else {
LOG &&
LOG &&
/* eslint-disable-next-line no-console */
console.log('Titiler /register %cEndpoint error', 'color: red;', error);
throw error;
Expand Down Expand Up @@ -321,6 +322,7 @@ export function RasterTimeseries(props: RasterTimeseriesProps) {
changeStatus({ status: 'idle', context: STATUS_KEY.Layer });
};
}, [
colorMap,
stacCollection
// This hook depends on a series of properties, but whenever they change the
// `stacCollection` is guaranteed to change because a new STAC request is
Expand Down Expand Up @@ -358,7 +360,8 @@ export function RasterTimeseries(props: RasterTimeseriesProps) {
const tileParams = qs.stringify(
{
assets: 'cog_default',
...(sourceParams ?? {})
...(sourceParams ?? {}),
...colorMap && {colormap_name: colorMap}
},
// Temporary solution to pass different tile parameters for hls data
{
Expand Down Expand Up @@ -485,6 +488,7 @@ export function RasterTimeseries(props: RasterTimeseriesProps) {
};
}, [
mosaicUrl,
colorMap,
points,
minZoom,
haveSourceParamsChanged,
Expand Down
2 changes: 1 addition & 1 deletion app/scripts/components/common/map/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface BaseTimeseriesProps extends BaseGeneratorParams {
tileApiEndpoint?: string;
zoomExtent?: number[];
onStatusChange?: (result: { status: ActionStatus; id: string }) => void;
colorMap?: string;
}

// export interface ZarrTimeseriesProps extends BaseTimeseriesProps {
Expand All @@ -72,4 +73,3 @@ interface AssetUrlReplacement {
export interface CMRTimeseriesProps extends BaseTimeseriesProps {
assetUrlReplacements?: AssetUrlReplacement;
}

3 changes: 2 additions & 1 deletion app/scripts/components/exploration/atoms/datasets.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { datasetLayers, reconcileDatasets } from '../data-utils';
import { datasetLayers } from '../data-utils';
import { reconcileDatasets } from '../data-utils-no-faux-module';
import { TimelineDataset, TimelineDatasetForUrl } from '../types.d.ts';
import { atomWithUrlValueStability } from '$utils/params-location-atom/atom-with-url-value-stability';

Expand Down
12 changes: 12 additions & 0 deletions app/scripts/components/exploration/atoms/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,18 @@ export function useTimelineDatasetVisibility(
return useAtom(visibilityAtom);
}

export function useTimelineDatasetColormap(
datasetAtom: PrimitiveAtom<TimelineDataset>
) {
const colorMapAtom = useMemo(() => {
return focusAtom(datasetAtom, (optic) =>
optic.prop('settings').prop('colorMap')
);
}, [datasetAtom]);

return useAtom(colorMapAtom);
}

export const useTimelineDatasetAnalysis = (
datasetAtom: PrimitiveAtom<TimelineDataset>
) => {
Expand Down
11 changes: 11 additions & 0 deletions app/scripts/components/exploration/atoms/rescale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { atomWithUrlValueStability } from '$utils/params-location-atom/atom-with-url-value-stability';

const initialParams = new URLSearchParams(window.location.search);

export const reverseAtom = atomWithUrlValueStability<boolean>({
initialValue: initialParams.get('reverse') === 'true',
urlParam: 'reverse',
hydrate: (value) => value === 'true',
areEqual: (prev, next) => prev === next,
dehydrate: (value) => (value ? 'true' : 'false')
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import { glsp, themeVal } from '@devseed-ui/theme-provider';
import { Link } from 'react-router-dom';
import { timelineDatasetsAtom } from '../../atoms/datasets';
import {
reconcileDatasets,
datasetLayers,
allExploreDatasets
} from '../../data-utils';
reconcileDatasets
} from '../../data-utils-no-faux-module';
import { datasetLayers,
allExploreDatasets} from '../../data-utils';
import RenderModalHeader from './header';

import ModalFooterRender from './footer';
Expand Down
Loading

0 comments on commit f0a4ace

Please sign in to comment.