Select Git revision
AnnotationCreation.js
AnnotationCreation.js 17.41 KiB
import React, { useEffect, useLayoutEffect, useState } from 'react';
import PropTypes from 'prop-types';
import {
Button,
} from '@mui/material';
import { styled } from '@mui/material/styles';
import { v4 as uuid } from 'uuid';
import { exportStageSVG } from 'react-konva-to-svg';
import CompanionWindow from 'mirador/dist/es/src/containers/CompanionWindow';
import { OSDReferences } from 'mirador/dist/es/src/plugins/OSDReferences';
import { VideosReferences } from 'mirador/dist/es/src/plugins/VideosReferences';
import Tab from '@mui/material/Tab';
import HighlightAltIcon from '@mui/icons-material/HighlightAlt';
import LayersIcon from '@mui/icons-material/Layers';
import LocalOfferIcon from '@mui/icons-material/LocalOffer';
import HubIcon from '@mui/icons-material/Hub';
import { TabContext, TabList, TabPanel } from '@mui/lab';
import AnnotationDrawing from './annotationForm/AnnotationDrawing';
import WebAnnotation from './WebAnnotation';
import { secondsToHMS } from './utils';
import AnnotationFormContent from './annotationForm/AnnotationFormContent';
import AnnotationFormTime from './annotationForm/AnnotationFormTime';
import AnnotationFormDrawing from './annotationForm/AnnotationFormDrawing';
import { geomFromAnnoTarget, timeFromAnnoTarget } from './AnnotationCreationUtils';
const TARGET_VIEW = 'target';
const OVERLAY_VIEW = 'layer';
const TAG_VIEW = 'tag';
const MANIFEST_LINK_VIEW = 'link';
/** Component for creating annotations.
* Display in companion window when a manifest is open and an annoation created or edited */
function AnnotationCreation(props) {
const [toolState, setToolState] = useState({
activeTool: 'cursor',
closedMode: 'closed',
colorPopoverOpen: false,
currentColorType: false,
fillColor: 'rgba(255, 0, 0, 0.5)',
image: { id: null },
imageEvent: null,
lineWeightPopoverOpen: false,
popoverAnchorEl: null,
popoverLineWeightAnchorEl: null,
strokeColor: 'green',
strokeWidth: 3,
});
// Initial state setup
const [state, setState] = useState(() => {
let tstart;
let tend;
const annoState = {};
if (props.annotation) {
// annotation body
if (Array.isArray(props.annotation.body)) {
annoState.tags = [];
props.annotation.body.forEach((body) => {
if (body.purpose === 'tagging' && body.type === 'TextualBody') {
annoState.tags.push(body.value);
} else if (body.type === 'TextualBody') {
annoState.textBody = body.value;
} else if (body.type === 'Image') {
// annoState.textBody = body.value; // why text body here ???
annoState.image = body;
} else if (body.type === 'AnnotationTitle') {
annoState.title = body;
}
});
} else if (props.annotation.body.type === 'TextualBody') {
annoState.textBody = props.annotation.body.value;
} else if (props.annotation.body.type === 'Image') {
// annoState.textBody = props.annotation.body.value; // why text body here ???
annoState.image = props.annotation.body;
}
//
// drawing position
if (props.annotation.target.selector) {
if (Array.isArray(props.annotation.target.selector)) {
props.annotation.target.selector.forEach((selector) => {
if (selector.type === 'SvgSelector') {
annoState.svg = selector.value;
} else if (selector.type === 'FragmentSelector') {
// TODO proper fragment selector extraction
annoState.xywh = geomFromAnnoTarget(selector.value);
[tstart, tend] = timeFromAnnoTarget(selector.value);
}
});
} else {
annoState.svg = props.annotation.target.selector.value;
// TODO does this happen ? when ? where are fragments selectors ?
}
} else if (typeof props.annotation.target === 'string') {
annoState.xywh = geomFromAnnoTarget(props.annotation.target);
[tstart, tend] = timeFromAnnoTarget(props.annotation.target);
}
}
// If we don't have tstart setted, we are creating a new annotation.
// If we don't have tend setted, we set it at the end of the video.
// So Tstart is current time and Tend the end of the video
if (!tstart) {
tstart = props.currentTime ? Math.floor(props.currentTime) : 0;
tend = props.mediaVideo ? props.mediaVideo.props.canvas.__jsonld.duration : 0;
}
return {
...toolState,
mediaVideo: props.mediaVideo,
...annoState,
tend,
textEditorStateBustingKey: 0,
tstart,
valueTime: [0, 1],
valuetextTime: '',
};
});
const [shapes, setShapes] = useState([]);
const [scale, setScale] = useState(1);
const [value, setValue] = useState(TARGET_VIEW);
const { height, width } = props.mediaVideo ? props.mediaVideo : 0;
// TODO Check the effect to keep and remove the other
// Add a state to trigger redraw
const [windowSize, setWindowSize] = useState({
height: window.innerHeight,
width: window.innerWidth,
});
// Listen to window resize event
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
useEffect(() => {
}, [toolState.fillColor, toolState.strokeColor, toolState.strokeWidth]);
useLayoutEffect(() => {
}, [{ height, width }]);
/** */
const handleImgChange = (newUrl, imgRef) => {
setToolState((prevState) => ({
...prevState,
image: { ...prevState.image, id: newUrl },
}));
};
/** set annotation start time to current time */
const setTstartNow = () => {
setState((prevState) => ({
...prevState,
tstart: Math.floor(props.currentTime),
}));
};
/** set annotation end time to current time */
const setTendNow = () => {
setState((prevState) => ({
...prevState,
tend: Math.floor(props.currentTime),
}));
};
/**
* @param {number} newValueTime
*/
const setValueTime = (newValueTime) => {
setState((prevState) => ({
...prevState,
valueTime: newValueTime,
}));
};
const tabHandler = (event, TabIndex) => {
setValue(TabIndex);
};
/**
* Change from slider
* @param {Event} event
* @param {number} newValueTime
*/
const handleChangeTime = (event, newValueTime) => {
const timeStart = newValueTime[0];
const timeEnd = newValueTime[1];
updateTstart(timeStart);
updateTend(timeEnd);
setValueTime(newValueTime);
};
/** Change from Tstart HMS Input */
const updateTstart = (value) => {
if (value > state.tend) {
return;
}
setState((prevState) => ({
...prevState,
tstart: value,
...props.setCurrentTime(value),
}));
};
/** update annotation end time */
const updateTend = (value) => {
setState((prevState) => ({
...prevState,
tend: value,
}));
};
// eslint-disable-next-line require-jsdoc
const seekToTstart = () => {
setState((prevState) => ({
...prevState,
...props.setSeekTo(prevState.tstart),
...props.setCurrentTime(prevState.tstart),
}));
};
/** */
const updateGeometry = ({ svg, xywh }) => {
setState((prevState) => ({
...prevState,
svg,
xywh,
}));
};
/** */
const setShapeProperties = (options) => new Promise(() => {
if (options.fill) {
state.fillColor = options.fill;
}
if (options.strokeWidth) {
state.strokeWidth = options.strokeWidth;
}
if (options.stroke) {
state.strokeColor = options.stroke;
}
setState({ ...state });
});
/** */
const updateTextBody = (textBody) => {
setState((prevState) => ({
...prevState,
textBody,
}));
};
/**
* Get SVG picture containing all the stuff draw in the stage (Konva Stage).
* This image will be put in overlay of the iiif media
*/
const getSvg = async () => {
const stage = window.Konva.stages.find((s) => s.attrs.id === props.windowId);
const svg = await exportStageSVG(stage); // TODO clean
return svg;
};
/** Set color tool from current shape */
const setColorToolFromCurrentShape = (colorState) => {
setToolState((prevState) => ({
...prevState,
...colorState,
}));
}
/** update shapes with shapes from annotationDrawing */
const updateShapes = (newShapes) => {
setShapes(newShapes);
}
/** delete shape */
const deleteShape = (shapeId) => {
const newShapes = shapes.filter((shape) => shape.id !== shapeId);
setShapes(newShapes);
}
/**
* Validate form and save annotation
*/
const submitForm = async (e) => {
e.preventDefault();
// TODO Possibly problem of syncing
// TODO Improve this code
// If we are in edit mode, we have the transformer on the stage saved in the annotation
if (state.activeTool === 'edit') {
setState((prevState) => ({
...prevState,
activeTool: 'cursor',
}));
return;
}
const {
annotation,
canvases,
receiveAnnotation,
config,
} = props;
const {
title,
textBody,
image,
tags,
xywh,
tstart,
tend,
textEditorStateBustingKey,
} = state;
// TODO rename variable for better comprenhension
const svg = await getSvg();
const t = (tstart && tend) ? `${tstart},${tend}` : null;
const body = { value: (!textBody.length && t) ? `${secondsToHMS(tstart)} -> ${secondsToHMS(tend)}` : textBody };
// TODO promises not handled. Use promiseAll ?
canvases.forEach(async (canvas) => {
const storageAdapter = config.annotation.adapter(canvas.id);
const anno = new WebAnnotation({
body,
canvasId: canvas.id,
fragsel: {
t,
xywh,
},
id: (annotation && annotation.id) || `${uuid()}`,
image,
manifestId: canvas.options.resource.id,
svg,
tags,
title,
}).toJson();
if (annotation) {
storageAdapter.update(anno)
.then((annoPage) => {
receiveAnnotation(canvas.id, storageAdapter.annotationPageId, annoPage);
});
} else {
storageAdapter.create(anno)
.then((annoPage) => {
receiveAnnotation(canvas.id, storageAdapter.annotationPageId, annoPage);
});
}
});
// TODO check if we need other thing in state
setState({
image: { id: null },
svg: null,
tend: 0,
textBody: '',
textEditorStateBustingKey: textEditorStateBustingKey + 1,
title: '',
tstart: 0,
xywh: null,
});
};
/** */
const {
annotation,
closeCompanionWindow,
id,
windowId,
} = props;
const {
textBody,
tstart,
tend,
textEditorStateBustingKey,
valueTime,
} = state;
const {
activeTool,
fillColor,
strokeColor,
strokeWidth,
closedMode,
imageEvent,
} = toolState;
// TODO : Vérifier ce code, c'est étrange de comprarer un typeof à une chaine de caractère.
const mediaIsVideo = props.mediaVideo !== 'undefined';
if (mediaIsVideo) {
valueTime[0] = tstart;
valueTime[1] = tend;
}
const videoDuration = props.mediaVideo ? props.mediaVideo.props.canvas.__jsonld.duration : 0;
// TODO: L'erreur de "Ref" sur l'ouverture d'une image vient d'ici et plus particulièrement
// du useEffect qui prend en dépedance [overlay.containerWidth, overlay.canvasWidth]
const videoref = VideosReferences.get(windowId);
const osdref = OSDReferences.get(windowId);
let overlay = null;
if (videoref) {
overlay = videoref.canvasOverlay;
}
if (osdref) {
console.debug('osdref', osdref);
}
/** Change scale from container / canva */
const updateScale = () => {
setScale(overlay.containerWidth / overlay.canvasWidth);
};
useEffect(() => {
}, [overlay.containerWidth, overlay.canvasWidth]);
return (
// we need to get the width and height of the image to pass it to the annotation drawing component
<CompanionWindow
title={annotation ? 'Edit annotation' : 'New annotation'}
windowId={windowId}
id={id}
>
<StyledAnnotationDrawing
scale={scale}
activeTool={activeTool}
annotation={annotation}
fillColor={fillColor}
strokeColor={strokeColor}
strokeWidth={strokeWidth}
closed={closedMode === 'closed'}
updateGeometry={updateGeometry}
windowId={windowId}
player={mediaIsVideo ? props.mediaVideo : OSDReferences.get(windowId)}
// we need to pass the width and height of the image to the annotation drawing component
width={overlay ? overlay.containerWidth : 1920}
height={overlay ? overlay.containerHeight : 1080}
orignalWidth={overlay ? overlay.canvasWidth : 1920}
orignalHeight={overlay ? overlay.canvasHeight : 1080}
setShapeProperties={setShapeProperties}
updateScale={updateScale}
imageEvent={imageEvent}
setColorToolFromCurrentShape={setColorToolFromCurrentShape}
updateShapes={updateShapes}
shapes={shapes}
mediaVideo={props.mediaVideo}
/>
<StyledForm
onSubmit={submitForm}
>
<TabContext value={value}>
<TabList value={value} onChange={tabHandler} aria-label="icon tabs">
<StyledTab
icon={<HighlightAltIcon />}
aria-label="TargetSelector"
value={TARGET_VIEW}
/>
<StyledTab
icon={<LayersIcon />}
aria-label="TargetSelector"
value={OVERLAY_VIEW}
/>
<StyledTab
icon={<LocalOfferIcon />}
aria-label="TargetSelector"
value={TAG_VIEW}
/>
<StyledTab
icon={<HubIcon />}
aria-label="TargetSelector"
value={MANIFEST_LINK_VIEW}
/>
</TabList>
<StyledTabPanel
value={TARGET_VIEW}
>
{mediaIsVideo && (
<AnnotationFormTime
mediaIsVideo={mediaIsVideo}
videoDuration={videoDuration}
value={valueTime}
handleChangeTime={handleChangeTime}
windowid={windowId}
setTstartNow={setTstartNow}
tstart={tstart}
updateTstart={updateTstart}
setTendNow={setTendNow}
tend={tend}
updateTend={updateTend}
/>
)}
</StyledTabPanel>
<StyledTabPanel
value={OVERLAY_VIEW}
>
<AnnotationFormDrawing
toolState={toolState}
updateToolState={setToolState}
handleImgChange={handleImgChange}
shapes={shapes}
/>
</StyledTabPanel>
<StyledTabPanel
value={TAG_VIEW}
>
<AnnotationFormContent
textBody={textBody}
updateTextBody={updateTextBody}
textEditorStateBustingKey={textEditorStateBustingKey}
/>
</StyledTabPanel>
<StyledTabPanel
value={MANIFEST_LINK_VIEW}
/>
</TabContext>
<StyledButtonDivSaveOrCancel>
<Button onClick={closeCompanionWindow}>
Cancel
</Button>
<Button variant="contained" color="primary" type="submit">
Save
</Button>
</StyledButtonDivSaveOrCancel>
</StyledForm>
</CompanionWindow>
);
}
const StyledButtonDivSaveOrCancel = styled('div')(({ theme }) => ({
display: 'flex',
justifyContent: 'flex-end',
}));
const StyledForm = styled('form')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: '20px',
paddingBottom: theme.spacing(1),
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(1),
paddingTop: theme.spacing(2),
}));
const StyledTab = styled(Tab)(({ theme }) => ({
minWidth: '0px',
padding: '12px 8px',
}));
const StyledTabPanel = styled(TabPanel)(({ theme }) => ({
padding: '0',
}));
const StyledAnnotationDrawing = styled(AnnotationDrawing)(({ theme }) => ({
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: 'auto',
}));
AnnotationCreation.propTypes = {
// TODO proper web annotation type ?
annotation: PropTypes.object, // eslint-disable-line react/forbid-prop-types
canvases: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string,
index: PropTypes.number,
}),
),
closeCompanionWindow: PropTypes.func,
config: PropTypes.shape({
annotation: PropTypes.shape({
adapter: PropTypes.func,
defaults: PropTypes.objectOf(
PropTypes.oneOfType(
[PropTypes.bool, PropTypes.func, PropTypes.number, PropTypes.string],
),
),
}),
}).isRequired,
currentTime: PropTypes.oneOfType([PropTypes.number, PropTypes.instanceOf(null)]),
id: PropTypes.string.isRequired,
receiveAnnotation: PropTypes.func.isRequired,
setCurrentTime: PropTypes.func,
setSeekTo: PropTypes.func,
windowId: PropTypes.string.isRequired,
mediaVideo: PropTypes.object.isRequired,
};
AnnotationCreation.defaultProps = {
annotation: null,
canvases: [],
closeCompanionWindow: () => {
},
currentTime: null,
paused: true,
setCurrentTime: () => {
},
setSeekTo: () => {
},
};
export default AnnotationCreation;