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
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>
464 lines
18 KiB
TypeScript
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=" Å"
|
|
inputProps={{
|
|
step: 0.1,
|
|
min: 0.5,
|
|
max: 50.0,
|
|
type: 'number',
|
|
}}
|
|
/>
|
|
</Stack>
|
|
<br/>Resolution Ring
|
|
</Stack>
|
|
</Paper>
|
|
</Grid>
|
|
</Grid>
|
|
</div>
|
|
|
|
}
|
|
}
|
|
|
|
export default PreviewImage;
|