Files
Jungfraujoch/frontend/src/components/PreviewImage.tsx
T
leonarski_f 1c4dfd03e2
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 10m22s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 11m30s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 11m41s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 12m32s
Build Packages / Generate python client (push) Successful in 18s
Build Packages / Build documentation (push) Successful in 54s
Build Packages / Create release (push) Has been skipped
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 9m44s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 8m53s
Build Packages / build:rpm (rocky8) (push) Successful in 9m40s
Build Packages / build:rpm (rocky9) (push) Successful in 10m37s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 9m54s
Build Packages / Unit tests (push) Successful in 1h6m33s
v1.0.0-rc.123 (#30)
This is an UNSTABLE release.

* jfjoch_broker: Use newer version of Google Ceres for (potential) CUDA 13 compatibility
* jfjoch_broker: Improve performance of generating preview images, especially for large detectors (9M-16M)
* jfjoch_viewer: Improve performance of displaying images, especially for large detectors (9M-16M)
* jfjoch_viewer: Add more color schemes for better image readability
* HDF5: Common mutex for reading and writing HDF5 if both operations were to happen in the same executable
* HDF5: suppress warning if path (upstream group) doesn't exists when checking if leaf exists

Reviewed-on: #30
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
Co-committed-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-01-30 13:43:09 +01:00

464 lines
18 KiB
TypeScript

import React, {Component} from 'react';
import Paper from "@mui/material/Paper";
import {
Checkbox,
FormControlLabel,
Grid,
Input,
Radio,
RadioGroup,
Select,
Slider,
Stack
} from "@mui/material";
import {TransformComponent, TransformWrapper} from "react-zoom-pan-pinch";
import {color_scale} from "../openapi";
import InputLabel from "@mui/material/InputLabel";
import MenuItem from "@mui/material/MenuItem";
import FormControl from "@mui/material/FormControl";
type preview_settings = {
saturation: number;
show_spots: boolean;
show_roi: boolean;
jpeg_quality: number;
show_user_mask: boolean;
show_beam_center: boolean;
resolution_ring: number;
scale: color_scale;
image_id: number;
auto_contrast: boolean;
res_estimate: boolean;
};
type MyProps = {
measuring: boolean,
min_image_number: number | undefined,
max_image_number: number | undefined
}
type MyState = {
settings: preview_settings,
s: Blob | null,
s_url: string | null,
update: boolean,
connection_error: boolean,
image_id_mode: string,
image_id: number
}
function handleErrors(response: Response): Response {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
}
function stringToEnum(value: string): color_scale {
const enumValue = Object.values(color_scale).find(
(v) => v === value
) as color_scale;
// If no match is found, default to file_writer_format.NONE
return enumValue || color_scale.INDIGO;
}
class PreviewImage extends Component<MyProps, MyState> {
interval: ReturnType<typeof setInterval> | undefined;
state : MyState = {
s: null,
settings: {
saturation: 10,
jpeg_quality: 90,
show_spots: true,
show_roi: false,
show_user_mask: false,
show_beam_center: true,
resolution_ring: 0.5,
scale: color_scale.INDIGO,
image_id: -1,
auto_contrast: true,
res_estimate: true
},
s_url: null,
update: true,
connection_error: true,
image_id_mode: 'last',
image_id: 0
}
update_image_id_mode = (event: React.ChangeEvent<HTMLInputElement>) => {
let s : preview_settings = this.state.settings;
switch (event.target.value) {
case "last":
s.image_id = -1;
this.setState({settings: s, image_id_mode: event.target.value});
this.getValues(s);
break;
case "last_indexed":
s.image_id = -2;
this.setState({settings: s, image_id_mode: event.target.value});
this.getValues(s);
break;
case "select":
s.image_id = this.state.image_id;
this.setState({settings: s, image_id_mode: event.target.value});
this.getValues(s);
break;
}
}
updateToggle = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
update: event.target.checked
});
if (event.target.checked)
this.getValues();
}
setSaturation = (event: Event, newValue: number | number[]) => {
let s : preview_settings = {
...this.state.settings,
saturation: newValue as number
};
this.setState({settings: s});
this.getValues(s);
}
setSaturationText = (event: React.ChangeEvent<HTMLInputElement>) => {
let newValue = 0;
if (event.target.value)
newValue = Number(event.target.value);
if (newValue < 0)
newValue = 0;
let s : preview_settings = {
...this.state.settings,
saturation: newValue as number
};
this.setState({settings: s});
this.getValues(s);
};
showSpotsToggle = (event: React.ChangeEvent<HTMLInputElement>) => {
let s : preview_settings = {
...this.state.settings,
show_spots: event.target.checked
};
this.setState({settings: s});
this.getValues(s);
}
resEstToggle = (event: React.ChangeEvent<HTMLInputElement>) => {
let s: preview_settings = {
...this.state.settings,
res_estimate: event.target.checked
}
this.setState({settings: s});
this.getValues(s);
}
autoContrastToggle = (event: React.ChangeEvent<HTMLInputElement>) => {
let s: preview_settings = {
...this.state.settings,
auto_contrast: event.target.checked
}
this.setState({settings: s});
this.getValues(s);
}
showROIToggle = (event: React.ChangeEvent<HTMLInputElement>) => {
let s : preview_settings = {
...this.state.settings,
show_roi: event.target.checked
};
this.setState({settings: s});
this.getValues(s);
}
showBeamCenterToggle = (event: React.ChangeEvent<HTMLInputElement>) => {
let s : preview_settings = {
...this.state.settings,
show_beam_center: event.target.checked
};
this.setState({settings: s});
this.getValues(s);
}
setResolutionRing = (event: Event, newValue: number | number[]) => {
let s : preview_settings = {
...this.state.settings,
resolution_ring: newValue as number
};
this.setState({settings: s});
this.getValues(s);
}
setResolutionRingText = (event: React.ChangeEvent<HTMLInputElement>) => {
let newValue = 0.5;
if (event.target.value)
newValue = Number(event.target.value);
if (newValue < 0.5)
newValue = 0.5;
if (newValue > 50.0)
newValue = 50.0;
let s : preview_settings = {
...this.state.settings,
resolution_ring: newValue as number
};
this.setState({settings: s});
this.getValues(s);
};
setImageID = (event: Event, newValue: number | number[]) => {
if (this.state.image_id_mode == "select") {
let s : preview_settings = {
...this.state.settings,
image_id: newValue as number
};
this.setState({settings: s, image_id: newValue as number});
this.getValues(s);
} else {
this.setState({image_id: newValue as number});
}
}
setImageIdText = (event: React.ChangeEvent<HTMLInputElement>) => {
let newValue = 0;
if (event.target.value)
newValue = Number(event.target.value);
if (newValue < 0)
newValue = 0;
if (this.state.image_id_mode == "select") {
let s : preview_settings = {
...this.state.settings,
image_id: newValue as number
};
this.setState({settings: s, image_id: newValue as number});
this.getValues(s);
} else {
this.setState({image_id: newValue as number});
}
};
showUserMaskToggle = (event: React.ChangeEvent<HTMLInputElement>) => {
let s : preview_settings = {
...this.state.settings,
show_user_mask: event.target.checked
};
this.setState({settings: s});
this.getValues(s);
}
getImage(s: preview_settings,) {
let url = `/image_buffer/image.jpeg`;
url += `?id=${s.image_id}`;
if (!s.auto_contrast)
url += `&saturation=${s.saturation}`;
url += `&jpeg_quality=${s.jpeg_quality}`
url += `&show_spots=${s.show_spots}`
url += `&show_roi=${s.show_roi}`
url += `&show_user_mask=${s.show_user_mask}`;
if (s.res_estimate)
url += '&show_res_est=true';
else
url += `&show_res_ring=${s.resolution_ring}`;
url += `&show_beam_center=${s.show_beam_center}`;
url += `&color=${s.scale}`;
fetch(url, {method: "GET"})
.then(handleErrors)
.then(data => data.blob())
.then(data => {
const url = URL.createObjectURL(data);
let tmp = this.state.s_url;
this.setState({s: data, s_url: url, connection_error: false});
if (tmp !== null)
URL.revokeObjectURL(tmp);
}).catch(error => {
this.setState({connection_error: true});
})
}
getValues(s: preview_settings = this.state.settings) {
this.getImage(s);
}
componentDidMount() {
this.getValues();
this.interval = setInterval(() => {
if (this.state.update && this.props.measuring)
this.getValues()
}, 2000);
}
componentWillUnmount() {
clearInterval(this.interval);
let tmp = this.state.s_url;
if (tmp !== null)
URL.revokeObjectURL(tmp);
}
render() {
return <div>
<Grid container spacing={3}>
<Grid item xs={8}>
<Paper sx={{height: 1250, minWidth: 1200} }>
{(!this.state.connection_error && (this.state.s_url !== null)) ?
<Stack
direction="row"
justifyContent="center"
alignItems="center"
>
<TransformWrapper maxScale={32}>
<TransformComponent>
<img src={this.state.s_url} alt="Live preview"
style={{maxWidth: "100%", maxHeight: 900}}/>
</TransformComponent>
</TransformWrapper>
</Stack> : <div>Preview image not available</div>
}
</Paper>
</Grid>
<Grid item xs={4}>
<Paper sx={{width: "100%", height: "100%"}}>
<br/>
<Stack spacing={2} direction="column" sx={{
justifyContent: "center",
alignItems: "center"}} alignItems="center">
<div><strong>Preview image selection </strong></div>
<FormControl sx={{width:"83%"}}>
<RadioGroup value={this.state.image_id_mode} onChange={this.update_image_id_mode}>
<FormControlLabel value="last" control={<Radio />} label="Last image" />
<FormControlLabel value="last_indexed" control={<Radio />} label="Last indexed image" />
<FormControlLabel value="select" control={<Radio />} label="Choose image" />
</RadioGroup>
</FormControl>
<Stack spacing={3} direction="row" sx={{width:"83%"}}>
<Slider
value={this.state.image_id}
min={this.props.min_image_number ?? 0}
max={this.props.max_image_number ?? 0}
step={1}
onChange={this.setImageID}
disabled={this.state.image_id_mode !== "select"}
valueLabelDisplay="auto"/>
<Input
value={this.state.image_id}
onChange={this.setImageIdText}
disabled={this.state.image_id_mode !== "select"}
inputProps={{
step: 1,
min: this.props.min_image_number ?? 0,
max: this.props.max_image_number ?? 0,
type: 'number',
}}
/>
</Stack>
<div><strong>Preview image settings </strong></div>
<FormControl sx = {{width: "83%"}}>
<FormControlLabel control={
<Checkbox checked={this.state.update} onChange={this.updateToggle} name="Update"/>
} label="Auto-update image"/>
<FormControlLabel control={
<Checkbox checked={this.state.settings.auto_contrast} onChange={this.autoContrastToggle} name="Auto-contrast"/>
} label="Auto-contrast"/>
<FormControlLabel control={
<Checkbox checked={this.state.settings.show_spots} onChange={this.showSpotsToggle} name="Show spots"/>
} label="Show spots"/>
<FormControlLabel control={
<Checkbox checked={this.state.settings.show_roi} onChange={this.showROIToggle} name="Show ROI"/>
} label="Show ROI"/>
<FormControlLabel control={
<Checkbox checked={this.state.settings.res_estimate} onChange={this.resEstToggle} name="Show resolution estimation (auto ring)"/>
} label="Show resolution estimate (auto ring)"/>
<FormControlLabel control={
<Checkbox checked={this.state.settings.show_beam_center} onChange={this.showBeamCenterToggle} name="Show beam center"/>
} label="Show beam center"/>
<FormControlLabel control={
<Checkbox checked={this.state.settings.show_user_mask} onChange={this.showUserMaskToggle} name="Show user mask"/>
} label="Show user mask"/>
</FormControl>
<FormControl sx = {{width: "83%"}}>
<InputLabel id="color-map">Color map</InputLabel>
<Select
value={this.state.settings.scale}
variant="outlined"
label="Color map"
onChange={(event) => {
let s : preview_settings = {
...this.state.settings,
scale: stringToEnum(event.target.value)
};
this.setState({settings: s});
this.getValues(s);
}}
fullWidth
>
<MenuItem value={color_scale.INDIGO}>Indigo/white</MenuItem>
<MenuItem value={color_scale.HEAT}>Heat</MenuItem>
<MenuItem value={color_scale.VIRIDIS}>Viridis</MenuItem>
<MenuItem value={color_scale.MAGMA}>Magma</MenuItem>
<MenuItem value={color_scale.INFERNO}>Inferno</MenuItem>
<MenuItem value={color_scale.BW}>Black/white</MenuItem>
<MenuItem value={color_scale.WB}>White/black</MenuItem>
<MenuItem value={color_scale.GREEN}>Green</MenuItem>
</Select>
</FormControl>
<Stack spacing={3} direction="row" sx={{width:"83%"}}>
<Slider value={Number(this.state.settings.saturation)} min={1} max={200}
onChange={this.setSaturation}
disabled={this.state.settings.auto_contrast}
valueLabelDisplay="auto"/>
<Input
value={this.state.settings.saturation}
onChange={this.setSaturationText}
disabled={this.state.settings.auto_contrast}
inputProps={{
step: 1,
min: 0,
max: 200,
type: 'number',
}}
/>
</Stack><br/>Saturation level
<Stack spacing={3} direction="row" sx={{width:"83%"}}>
<Slider
value={(this.state.settings.resolution_ring === undefined) ? 0.5 : Number(this.state.settings.resolution_ring)}
min={0.5} max={10.0} step={0.1}
onChange={this.setResolutionRing} valueLabelDisplay="auto"
disabled={this.state.settings.res_estimate}/>
<Input
value={this.state.settings.resolution_ring}
disabled={this.state.settings.res_estimate}
onChange={this.setResolutionRingText}
endAdornment="&nbsp;&nbsp;Å"
inputProps={{
step: 0.1,
min: 0.5,
max: 50.0,
type: 'number',
}}
/>
</Stack>
<br/>Resolution Ring
</Stack>
</Paper>
</Grid>
</Grid>
</div>
}
}
export default PreviewImage;