Files
Jungfraujoch/frontend/src/components/PreviewImage.tsx
2025-06-22 13:15:10 +02:00

445 lines
17 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;
use_png: 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,
use_png: 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);
}
showROIToggle = (event: React.ChangeEvent<HTMLInputElement>) => {
let s : preview_settings = {
...this.state.settings,
show_roi: event.target.checked
};
this.setState({settings: s});
this.getValues(s);
}
usePNGToggle = (event: React.ChangeEvent<HTMLInputElement>) => {
let s : preview_settings = {
...this.state.settings,
use_png: 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.`;
if (s.use_png)
url += "png";
else
url += "jpeg";
url += `?id=${s.image_id}`;
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}`;
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.use_png} onChange={this.usePNGToggle} name="Use PNG format"/>
} label="PNG image format (slower, but lossless)"/>
<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.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.BW}>Black/white</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} valueLabelDisplay="auto"/>
<Input
value={this.state.settings.saturation}
onChange={this.setSaturationText}
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"/>
<Input
value={this.state.settings.resolution_ring}
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;