Skip to content

Commit

Permalink
Merge pull request #328 from xcube-dev/forman-user_color_bars
Browse files Browse the repository at this point in the history
User-defined color bars
  • Loading branch information
forman authored May 3, 2024
2 parents 9480d4e + 5ca43c2 commit 37529cb
Show file tree
Hide file tree
Showing 31 changed files with 1,813 additions and 175 deletions.
25 changes: 22 additions & 3 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## Changes in version 1.2.0 (in development)

* Users can now define their own color bars.
This feature requires xcube server >= 1.6.

* Introduced overlay layers that can be selected in the settings.

* Users can now define their own base maps and overlay layers.
Expand All @@ -22,7 +25,10 @@
same dataset, if switched on. Layer opacity only affects the variable
layer, not the RGB layer.

* Numerous changes regard development environment renewal and
* Fixed problem with color bar selector that occurred if a variable
used an unknown color bar name.

* Numerous changes regarding development environment renewal and
code quality improvements:

- Changed the development environment from [create-react-app](https://create-react-app.dev/)
Expand All @@ -36,10 +42,23 @@
- Applied new MUI v5 styling defaults.




TODO:

* User color bars:
- Harmonize styling see useItemStyles.ts, search for COLOR_BAR_ITEM_GAP
- Support categorical user color bars
- ~Implement user color bars in xcube server and adjust tile API calls
in viewer~
- ~Use RGBA instead of just RGB~
- ~Fix code duplication, search for COLOR_BAR_ITEM_BOX_MARGIN~
- ~Fix popover toolbar for editing and removing a color bar~
- ~Use image data and "img" for rendering instead of "canvas"~
- ~No longer display code parsing error messages in image,
display as tooltip instead and use error border in text field~
- ~Fix generation of base64 image data~
- ~Fix selecting a user color bar~
- ~Allow for same values in color bars code~

* Allow running `run: npm run coverage` in `.github/workflows/ci.yaml`:

## Changes in version 1.1.1
Expand Down
108 changes: 100 additions & 8 deletions src/actions/controlActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
PlaceGroup,
} from "@/model/place";
import { Time, TimeRange } from "@/model/timeSeries";
import { renderUserColorBarAsBase64, UserColorBar } from "@/model/userColorBar";
import {
selectedDatasetIdSelector,
selectedDatasetSelectedPlaceGroupsSelector,
Expand Down Expand Up @@ -406,14 +407,6 @@ export interface SelectTimeSeriesUpdateMode {
timeSeriesUpdateMode: "add" | "replace";
}

// TODO: check, if we can remove this action, seems not in use

export function selectTimeSeriesUpdateMode(
timeSeriesUpdateMode: "add" | "replace",
): SelectTimeSeriesUpdateMode {
return { type: SELECT_TIME_SERIES_UPDATE_MODE, timeSeriesUpdateMode };
}

////////////////////////////////////////////////////////////////////////////////

export const UPDATE_TIME_ANIMATION = "UPDATE_TIME_ANIMATION";
Expand Down Expand Up @@ -624,6 +617,102 @@ export function updateSettings(

////////////////////////////////////////////////////////////////////////////////

export function addUserColorBar(colorBarId: string) {
return (dispatch: Dispatch) => {
dispatch(_addUserColorBar(colorBarId));
dispatch(updateUserColorBarImageDataById(colorBarId) as unknown as Action);
};
}

export const ADD_USER_COLOR_BAR = "ADD_USER_COLOR_BAR";

export interface AddUserColorBar {
type: typeof ADD_USER_COLOR_BAR;
colorBarId: string;
}

export function _addUserColorBar(colorBarId: string): AddUserColorBar {
return { type: ADD_USER_COLOR_BAR, colorBarId };
}

////////////////////////////////////////////////////////////////////////////////

export const REMOVE_USER_COLOR_BAR = "REMOVE_USER_COLOR_BAR";

export interface RemoveUserColorBar {
type: typeof REMOVE_USER_COLOR_BAR;
colorBarId: string;
}

export function removeUserColorBar(colorBarId: string): RemoveUserColorBar {
return { type: REMOVE_USER_COLOR_BAR, colorBarId };
}

////////////////////////////////////////////////////////////////////////////////

export function updateUserColorBar(userColorBar: UserColorBar) {
return (dispatch: Dispatch) => {
dispatch(_updateUserColorBar(userColorBar));
dispatch(updateUserColorBarImageData(userColorBar) as unknown as Action);
};
}

export const UPDATE_USER_COLOR_BAR = "UPDATE_USER_COLOR_BAR";

export interface UpdateUserColorBar {
type: typeof UPDATE_USER_COLOR_BAR;
userColorBar: UserColorBar;
}

export function _updateUserColorBar(
userColorBar: UserColorBar,
): UpdateUserColorBar {
return { type: UPDATE_USER_COLOR_BAR, userColorBar };
}

////////////////////////////////////////////////////////////////////////////////

function updateUserColorBarImageDataById(colorBarId: string) {
return (dispatch: Dispatch, getState: () => AppState) => {
const colorBar = getState().controlState.userColorBars.find(
(ucb) => ucb.id === colorBarId,
);
if (colorBar) {
dispatch(updateUserColorBarImageData(colorBar) as unknown as Action);
}
};
}

function updateUserColorBarImageData(colorBar: UserColorBar) {
return (dispatch: Dispatch) => {
renderUserColorBarAsBase64(colorBar).then(({ imageData, errorMessage }) => {
dispatch(_updateUserColorBar({ ...colorBar, imageData, errorMessage }));
});
};
}

////////////////////////////////////////////////////////////////////////////////

export function updateUserColorBarsImageData() {
return (dispatch: Dispatch, getState: () => AppState) => {
getState().controlState.userColorBars.forEach((colorBar) => {
if (!colorBar.imageData) {
dispatch(updateUserColorBarImageData(colorBar) as unknown as Action);
}
});
};
}

////////////////////////////////////////////////////////////////////////////////

export function updateUserColorBars(
userColorBars: UserColorBar[],
): UpdateSettings {
return { type: UPDATE_SETTINGS, settings: { userColorBars } };
}

////////////////////////////////////////////////////////////////////////////////

export type ControlAction =
| SelectDataset
| UpdateDatasetPlaceGroup
Expand All @@ -641,6 +730,9 @@ export type ControlAction =
| AddActivity
| RemoveActivity
| ChangeLocale
| AddUserColorBar
| RemoveUserColorBar
| UpdateUserColorBar
| UpdateSettings
| OpenDialog
| CloseDialog
Expand Down
39 changes: 20 additions & 19 deletions src/components/ColorBarLegend/ColorBarCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,27 @@
* SOFTWARE.
*/

import React, { useEffect, useRef } from "react";
import { useEffect, useRef, MouseEvent } from "react";

import i18n from "@/i18n";
import { ColorBar, renderColorBar } from "@/model/colorBar";
import Tooltip from "@mui/material/Tooltip";

interface ColorBarCanvasProps {
colorBar: ColorBar;
opacity: number;
width: number | string | undefined;
height: number | string | undefined;
onClick: (event: React.MouseEvent<HTMLCanvasElement>) => void;
onClick: (event: MouseEvent<HTMLCanvasElement>) => void;
}

const ColorBarCanvas: React.FC<ColorBarCanvasProps> = ({
export default function ColorBarCanvas({
colorBar,
opacity,
width,
height,
onClick,
}) => {
}: ColorBarCanvasProps) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);

useEffect(() => {
Expand All @@ -51,20 +52,20 @@ const ColorBarCanvas: React.FC<ColorBarCanvasProps> = ({
}
}, [colorBar, opacity]);

const { baseName, imageData } = colorBar;
const tooltipTitle = imageData
? baseName
: i18n.get("Unknown color bar") + `: ${baseName}`;

return (
<>
{colorBar.imageData ? (
<canvas
ref={canvasRef}
width={width || 240}
height={height || 24}
onClick={onClick}
/>
) : (
<div>{i18n.get("Unknown color bar") + `: ${colorBar.baseName}`}</div>
)}
</>
<Tooltip title={tooltipTitle}>
<canvas
ref={canvasRef}
width={width || 240}
height={height || 24}
onClick={onClick}
style={!imageData ? { border: "0.5px solid red" } : undefined}
/>
</Tooltip>
);
};

export default ColorBarCanvas;
}
36 changes: 28 additions & 8 deletions src/components/ColorBarLegend/ColorBarColorEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,16 @@ import { Theme } from "@mui/material";
import Box from "@mui/material/Box";

import { ColorBar, ColorBars } from "@/model/colorBar";
import ColorBarStyleEditor from "@/components/ColorBarLegend/ColorBarStyleEditor";
import ColorBarSelect from "@/components/ColorBarLegend/ColorBarSelect";
import { UserColorBar } from "@/model/userColorBar";
import ColorBarStyleEditor from "./ColorBarStyleEditor";
import ColorBarSelect from "./ColorBarSelect";
import { COLOR_BAR_ITEM_GAP } from "./useItemStyles";

const COLOR_BAR_BOX_MARGIN = 1;
const COLOR_BAR_ITEM_BOX_MARGIN = 0.2;

const useStyles = makeStyles((theme: Theme) => ({
colorBarBox: {
marginTop: theme.spacing(
COLOR_BAR_BOX_MARGIN - 2 * COLOR_BAR_ITEM_BOX_MARGIN,
),
marginTop: theme.spacing(COLOR_BAR_BOX_MARGIN - 2 * COLOR_BAR_ITEM_GAP),
marginLeft: theme.spacing(COLOR_BAR_BOX_MARGIN),
marginRight: theme.spacing(COLOR_BAR_BOX_MARGIN),
marginBottom: theme.spacing(COLOR_BAR_BOX_MARGIN),
Expand All @@ -55,15 +54,36 @@ interface ColorBarColorEditorProps {
opacity: number,
) => void;
colorBars: ColorBars;
userColorBars: UserColorBar[];
addUserColorBar: (userColorBarId: string) => void;
removeUserColorBar: (userColorBarId: string) => void;
updateUserColorBar: (userColorBar: UserColorBar) => void;
updateUserColorBars: (userColorBars: UserColorBar[]) => void;
}

export default function ColorBarColorEditor(props: ColorBarColorEditorProps) {
const classes = useStyles();
const { colorBars, ...baseProps } = props;
const {
colorBars,
userColorBars,
addUserColorBar,
removeUserColorBar,
updateUserColorBar,
updateUserColorBars,
...baseProps
} = props;
return (
<Box className={classes.colorBarBox}>
<ColorBarStyleEditor {...baseProps} />
<ColorBarSelect {...baseProps} colorBars={colorBars} />
<ColorBarSelect
{...baseProps}
colorBars={colorBars}
userColorBars={userColorBars}
addUserColorBar={addUserColorBar}
removeUserColorBar={removeUserColorBar}
updateUserColorBar={updateUserColorBar}
updateUserColorBars={updateUserColorBars}
/>
</Box>
);
}
60 changes: 60 additions & 0 deletions src/components/ColorBarLegend/ColorBarGroupComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2019-2024 by the xcube development team and contributors.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

import { ColorBarGroup } from "@/model/colorBar";
import ColorBarGroupHeader from "./ColorBarGroupHeader";
import ColorBarItem from "./ColorBarItem";

interface ColorBarGroupComponentProps {
colorBarGroup: ColorBarGroup;
selectedColorBarName: string | null;
onSelectColorBar: (colorBarName: string) => void;
images: Record<string, string>;
}

export default function ColorBarGroupComponent({
colorBarGroup,
selectedColorBarName,
onSelectColorBar,
images,
}: ColorBarGroupComponentProps) {
return (
<>
<ColorBarGroupHeader
title={colorBarGroup.title}
description={colorBarGroup.description}
/>
{colorBarGroup.names.map((name) => (
<ColorBarItem
key={name}
title={name}
imageData={images[name]}
selected={name === selectedColorBarName}
onSelect={() => onSelectColorBar(name)}
width={240}
/>
))}
</>
);
}
Loading

0 comments on commit 37529cb

Please sign in to comment.