Draw the resolution ring and azimuthal-ROI outlines by sweeping ResPhiToPxl through the geometry (a shared DrawArc/DrawThickLine helper), so they trace the true conic on a tilted detector instead of PONI-centred circles. Draw the beam crosshair at GetDirectBeam_pxl() (the real beam/detector intersection) rather than the stored PONI. Unify the spot overlay colours with the viewer: green = not indexed, magenta = indexed (primary lattice), cyan = ice ring, coral = secondary/further lattice. Add Bragg-prediction overlay (dark-red circles, skipping systematic absences) gated on a new show_predictions preview setting, wired through the OpenAPI spec, the image.jpeg HTTP handler, and the frontend toggle (+regenerated TS client). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
436 lines
17 KiB
TypeScript
436 lines
17 KiB
TypeScript
import {ChangeEvent, memo, useEffect, useRef, useState} 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 "../client";
|
|
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_predictions: 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
|
|
}
|
|
|
|
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 the indigo color scale
|
|
return enumValue || color_scale.INDIGO;
|
|
}
|
|
|
|
const default_preview_settings: preview_settings = {
|
|
saturation: 10,
|
|
jpeg_quality: 90,
|
|
show_spots: true,
|
|
show_predictions: false,
|
|
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
|
|
};
|
|
|
|
function PreviewImage({measuring, min_image_number, max_image_number}: MyProps) {
|
|
const [settings, setSettings] = useState<preview_settings>(default_preview_settings);
|
|
const [sUrl, setSUrl] = useState<string | null>(null);
|
|
const [update, setUpdate] = useState(true);
|
|
const [connectionError, setConnectionError] = useState(true);
|
|
const [imageIdMode, setImageIdMode] = useState('last');
|
|
const [imageId, setImageId] = useState(0);
|
|
|
|
// Refs let the polling interval read the latest settings without being
|
|
// re-created on every settings change, and let cleanup revoke the live URL.
|
|
const sUrlRef = useRef<string | null>(null);
|
|
const settingsRef = useRef(settings);
|
|
settingsRef.current = settings;
|
|
|
|
const 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_predictions=${s.show_predictions}`
|
|
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 objectUrl = URL.createObjectURL(data);
|
|
const tmp = sUrlRef.current;
|
|
sUrlRef.current = objectUrl;
|
|
setSUrl(objectUrl);
|
|
setConnectionError(false);
|
|
if (tmp !== null)
|
|
URL.revokeObjectURL(tmp);
|
|
}).catch(error => {
|
|
setConnectionError(true);
|
|
})
|
|
};
|
|
|
|
const getValues = (s: preview_settings = settings) => {
|
|
getImage(s);
|
|
};
|
|
|
|
// Initial fetch + revoke the live object URL on unmount.
|
|
useEffect(() => {
|
|
getImage(settingsRef.current);
|
|
return () => {
|
|
if (sUrlRef.current !== null)
|
|
URL.revokeObjectURL(sUrlRef.current);
|
|
};
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
// Auto-update poll; reads the latest settings via ref.
|
|
useEffect(() => {
|
|
const id = setInterval(() => {
|
|
if (update && measuring)
|
|
getImage(settingsRef.current);
|
|
}, 2000);
|
|
return () => clearInterval(id);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [update, measuring]);
|
|
|
|
const update_image_id_mode = (event: ChangeEvent<HTMLInputElement>) => {
|
|
let image_id : number;
|
|
switch (event.target.value) {
|
|
case "last":
|
|
image_id = -1;
|
|
break;
|
|
case "last_indexed":
|
|
image_id = -2;
|
|
break;
|
|
case "select":
|
|
image_id = imageId;
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
let s : preview_settings = {...settings, image_id};
|
|
setSettings(s);
|
|
setImageIdMode(event.target.value);
|
|
getValues(s);
|
|
};
|
|
|
|
const updateToggle = (event: ChangeEvent<HTMLInputElement>) => {
|
|
setUpdate(event.target.checked);
|
|
if (event.target.checked)
|
|
getValues();
|
|
};
|
|
|
|
const setSaturation = (event: Event, newValue: number | number[]) => {
|
|
let s : preview_settings = {...settings, saturation: newValue as number};
|
|
setSettings(s);
|
|
getValues(s);
|
|
};
|
|
|
|
const setSaturationText = (event: ChangeEvent<HTMLInputElement>) => {
|
|
let newValue = 0;
|
|
if (event.target.value)
|
|
newValue = Number(event.target.value);
|
|
if (newValue < 0)
|
|
newValue = 0;
|
|
|
|
let s : preview_settings = {...settings, saturation: newValue as number};
|
|
setSettings(s);
|
|
getValues(s);
|
|
};
|
|
|
|
const showSpotsToggle = (event: ChangeEvent<HTMLInputElement>) => {
|
|
let s : preview_settings = {...settings, show_spots: event.target.checked};
|
|
setSettings(s);
|
|
getValues(s);
|
|
};
|
|
|
|
const showPredictionsToggle = (event: ChangeEvent<HTMLInputElement>) => {
|
|
let s : preview_settings = {...settings, show_predictions: event.target.checked};
|
|
setSettings(s);
|
|
getValues(s);
|
|
};
|
|
|
|
const resEstToggle = (event: ChangeEvent<HTMLInputElement>) => {
|
|
let s: preview_settings = {...settings, res_estimate: event.target.checked};
|
|
setSettings(s);
|
|
getValues(s);
|
|
};
|
|
|
|
const autoContrastToggle = (event: ChangeEvent<HTMLInputElement>) => {
|
|
let s: preview_settings = {...settings, auto_contrast: event.target.checked};
|
|
setSettings(s);
|
|
getValues(s);
|
|
};
|
|
|
|
const showROIToggle = (event: ChangeEvent<HTMLInputElement>) => {
|
|
let s : preview_settings = {...settings, show_roi: event.target.checked};
|
|
setSettings(s);
|
|
getValues(s);
|
|
};
|
|
|
|
const showBeamCenterToggle = (event: ChangeEvent<HTMLInputElement>) => {
|
|
let s : preview_settings = {...settings, show_beam_center: event.target.checked};
|
|
setSettings(s);
|
|
getValues(s);
|
|
};
|
|
|
|
const setResolutionRing = (event: Event, newValue: number | number[]) => {
|
|
let s : preview_settings = {...settings, resolution_ring: newValue as number};
|
|
setSettings(s);
|
|
getValues(s);
|
|
};
|
|
|
|
const setResolutionRingText = (event: 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 = {...settings, resolution_ring: newValue as number};
|
|
setSettings(s);
|
|
getValues(s);
|
|
};
|
|
|
|
const setImageIDSlider = (event: Event, newValue: number | number[]) => {
|
|
if (imageIdMode == "select") {
|
|
let s : preview_settings = {...settings, image_id: newValue as number};
|
|
setSettings(s);
|
|
setImageId(newValue as number);
|
|
getValues(s);
|
|
} else {
|
|
setImageId(newValue as number);
|
|
}
|
|
};
|
|
|
|
const setImageIdText = (event: ChangeEvent<HTMLInputElement>) => {
|
|
let newValue = 0;
|
|
if (event.target.value)
|
|
newValue = Number(event.target.value);
|
|
if (newValue < 0)
|
|
newValue = 0;
|
|
|
|
if (imageIdMode == "select") {
|
|
let s : preview_settings = {...settings, image_id: newValue as number};
|
|
setSettings(s);
|
|
setImageId(newValue as number);
|
|
getValues(s);
|
|
} else {
|
|
setImageId(newValue as number);
|
|
}
|
|
};
|
|
|
|
const showUserMaskToggle = (event: ChangeEvent<HTMLInputElement>) => {
|
|
let s : preview_settings = {...settings, show_user_mask: event.target.checked};
|
|
setSettings(s);
|
|
getValues(s);
|
|
};
|
|
|
|
return <div>
|
|
|
|
<Grid container spacing={3}>
|
|
|
|
<Grid item xs={8}>
|
|
<Paper sx={{width: "100%", height: "100%", p: 1,
|
|
display: "flex", justifyContent: "center", alignItems: "center"}}>
|
|
{(!connectionError && (sUrl !== null)) ?
|
|
<Stack
|
|
direction="row"
|
|
justifyContent="center"
|
|
alignItems="center"
|
|
sx={{width: "100%"}}
|
|
>
|
|
<TransformWrapper maxScale={32}>
|
|
<TransformComponent>
|
|
<img src={sUrl} 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={imageIdMode} onChange={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={imageId}
|
|
min={min_image_number ?? 0}
|
|
max={max_image_number ?? 0}
|
|
step={1}
|
|
onChange={setImageIDSlider}
|
|
disabled={imageIdMode !== "select"}
|
|
valueLabelDisplay="auto"/>
|
|
<Input
|
|
value={imageId}
|
|
onChange={setImageIdText}
|
|
disabled={imageIdMode !== "select"}
|
|
inputProps={{
|
|
step: 1,
|
|
min: min_image_number ?? 0,
|
|
max: max_image_number ?? 0,
|
|
type: 'number',
|
|
}}
|
|
/>
|
|
</Stack>
|
|
<div><strong>Preview image settings </strong></div>
|
|
<FormControl sx = {{width: "83%"}}>
|
|
|
|
<FormControlLabel control={
|
|
<Checkbox checked={update} onChange={updateToggle} name="Update"/>
|
|
} label="Auto-update image"/>
|
|
<FormControlLabel control={
|
|
<Checkbox checked={settings.auto_contrast} onChange={autoContrastToggle} name="Auto-contrast"/>
|
|
} label="Auto-contrast"/>
|
|
<FormControlLabel control={
|
|
<Checkbox checked={settings.show_spots} onChange={showSpotsToggle} name="Show spots"/>
|
|
} label="Show spots"/>
|
|
<FormControlLabel control={
|
|
<Checkbox checked={settings.show_predictions} onChange={showPredictionsToggle} name="Show predictions"/>
|
|
} label="Show predictions"/>
|
|
<FormControlLabel control={
|
|
<Checkbox checked={settings.show_roi} onChange={showROIToggle} name="Show ROI"/>
|
|
} label="Show ROI"/>
|
|
<FormControlLabel control={
|
|
<Checkbox checked={settings.res_estimate} onChange={resEstToggle} name="Show resolution estimation (auto ring)"/>
|
|
} label="Show resolution estimate (auto ring)"/>
|
|
<FormControlLabel control={
|
|
<Checkbox checked={settings.show_beam_center} onChange={showBeamCenterToggle} name="Show beam center"/>
|
|
} label="Show beam center"/>
|
|
<FormControlLabel control={
|
|
<Checkbox checked={settings.show_user_mask} onChange={showUserMaskToggle} name="Show user mask"/>
|
|
} label="Show user mask"/>
|
|
</FormControl>
|
|
<FormControl sx = {{width: "83%"}}>
|
|
<InputLabel id="color-map">Color map</InputLabel>
|
|
<Select
|
|
value={settings.scale}
|
|
variant="outlined"
|
|
label="Color map"
|
|
onChange={(event) => {
|
|
let s : preview_settings = {...settings, scale: stringToEnum(event.target.value)};
|
|
setSettings(s);
|
|
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(settings.saturation)} min={1} max={200}
|
|
onChange={setSaturation}
|
|
disabled={settings.auto_contrast}
|
|
valueLabelDisplay="auto"/>
|
|
|
|
<Input
|
|
value={settings.saturation}
|
|
onChange={setSaturationText}
|
|
disabled={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={(settings.resolution_ring === undefined) ? 0.5 : Number(settings.resolution_ring)}
|
|
min={0.5} max={10.0} step={0.1}
|
|
onChange={setResolutionRing} valueLabelDisplay="auto"
|
|
disabled={settings.res_estimate}/>
|
|
<Input
|
|
value={settings.resolution_ring}
|
|
disabled={settings.res_estimate}
|
|
onChange={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 memo(PreviewImage);
|