Skip to content
Snippets Groups Projects
Select Git revision
  • 146497245f9125f93af21678ec62d43cb2d9eef5
  • mui5-tetras-main-stable default protected
  • mui5-tetras-main-old-stable
  • preprod protected
  • 75-dernieres-ameliorations-avant-workshop-du-7-02
  • wip-fix-xywh
  • wip-positionement-annot
  • wip-surface-transformer
  • uploads-file
  • 69-la-video-demare-quand-on-fait-glisser-le-slider-et-le-clic-creer-un-decalage-entre-le-player
  • 61-recettage-des-outils-d-annotation
  • gestion_multiple_ouverture_pannel_annotation
  • autorisation_un_pannel_annotation
  • autorisation_un_pannel_edition_annotation
  • récupération_temps_video
  • save-shapes-and-position
  • fix-error-create-annotation-pannel
  • time-saving-on-annotation
  • tetras-main protected
  • fix-poc-mirador
  • tetras-antho-test
21 results

AnnotationCreation.js

Blame
  • 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;