Skip to content
Snippets Groups Projects
Select Git revision
  • 06d4081e17077b9b6c0510ae4d55afbb65f6d6b7
  • master default protected
  • multiprocessing
  • experiment/clara
  • experiment/spec2B-poc
  • experiment/qivalio-poc
  • experiment/ertms
  • MAY-2023
  • FEB-2023
  • EGC-2023
  • 0.2.1
  • v0.2.0
  • v0.1.2
13 results

test_rule_phenomena_mod_analyzer.py

Blame
  • AnnotationCreation.js 20.92 KiB
    import React, { Component } from 'react';
    import PropTypes from 'prop-types';
    import {
      Button, Paper, Grid, Popover, Divider,
      MenuList, MenuItem, ClickAwayListener,
    } from '@material-ui/core';
    import { Alarm, LastPage } from '@material-ui/icons';
    import Typography from '@material-ui/core/Typography';
    import ToggleButton from '@material-ui/lab/ToggleButton';
    import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup';
    import RectangleIcon from '@material-ui/icons/CheckBoxOutlineBlank';
    import CircleIcon from '@material-ui/icons/RadioButtonUnchecked';
    import PolygonIcon from '@material-ui/icons/Timeline';
    import GestureIcon from '@material-ui/icons/Gesture';
    import ClosedPolygonIcon from '@material-ui/icons/ChangeHistory';
    import OpenPolygonIcon from '@material-ui/icons/ShowChart';
    import FormatColorFillIcon from '@material-ui/icons/FormatColorFill';
    import StrokeColorIcon from '@material-ui/icons/BorderColor';
    import LineWeightIcon from '@material-ui/icons/LineWeight';
    import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
    import FormatShapesIcon from '@material-ui/icons/FormatShapes';
    import TextField from '@material-ui/core/TextField';
    import { SketchPicker } from 'react-color';
    import { v4 as uuid } from 'uuid';
    import { withStyles } from '@material-ui/core/styles';
    import CompanionWindow from 'mirador/dist/es/src/containers/CompanionWindow';
    import { VideosReferences } from 'mirador/dist/es/src/plugins/VideosReferences';
    import { OSDReferences } from 'mirador/dist/es/src/plugins/OSDReferences';
    import AnnotationDrawing from './AnnotationDrawing';
    import TextEditor from './TextEditor';
    import WebAnnotation from './WebAnnotation';
    import CursorIcon from './icons/Cursor';
    import HMSInput from './HMSInput';
    import ImageFormField from './ImageFormField';
    import { secondsToHMS } from './utils';
    
    /** Extract time information from annotation target */
    function timeFromAnnoTarget(annotarget) {
      console.info('TODO proper time extraction from: ', annotarget);
      // TODO w3c media fragments: t=,10 t=5,
      const r = /t=([0-9.]+),([0-9.]+)/.exec(annotarget);
      if (!r || r.length !== 3) {
        return [0, 0];
      }
      return [Number(r[1]), Number(r[2])];
    }
    
    /** Extract xywh from annotation target */
    function geomFromAnnoTarget(annotarget) {
      console.info('TODO proper xywh extraction from: ', annotarget);
      const r = /xywh=((-?[0-9]+,?)+)/.exec(annotarget);
      if (!r || r.length !== 3) {
        return '';
      }
      return r[1];
    }
    
    /** */
    class AnnotationCreation extends Component {
      /** */
      constructor(props) {
        super(props);
    
        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 (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);
                  [annoState.tstart, annoState.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);
            [annoState.tstart, annoState.tend] = timeFromAnnoTarget(props.annotation.target);
          }
        }
    
        const toolState = {
          activeTool: 'cursor',
          closedMode: 'closed',
          colorPopoverOpen: false,
          currentColorType: false,
          fillColor: null,
          strokeColor: '#00BFFF',
          strokeWidth: 3,
          ...(props.config.annotation.defaults || {}),
        };
    
        const timeState = props.currentTime !== null
          ? { tend: Math.floor(props.currentTime) + 10, tstart: Math.floor(props.currentTime) }
          : { tend: null, tstart: null };
    
        this.state = {
          ...toolState,
          ...timeState,
          activeTool: 'cursor',
          closedMode: 'closed',
          currentColorType: false,
          fillColor: null,
          image: { id: null },
          lineWeightPopoverOpen: false,
          popoverAnchorEl: null,
          popoverLineWeightAnchorEl: null,
          svg: null,
          textBody: '',
          textEditorStateBustingKey: 0,
          xywh: null,
          ...annoState,
        };
    
        this.submitForm = this.submitForm.bind(this);
        // this.updateBody = this.updateBody.bind(this);
        this.updateTextBody = this.updateTextBody.bind(this);
        this.updateTstart = this.updateTstart.bind(this);
        this.updateTend = this.updateTend.bind(this);
        this.setTstartNow = this.setTstartNow.bind(this);
        this.setTendNow = this.setTendNow.bind(this);
        this.seekToTstart = this.seekToTstart.bind(this);
        this.seekToTend = this.seekToTend.bind(this);
        this.updateGeometry = this.updateGeometry.bind(this);
        this.changeTool = this.changeTool.bind(this);
        this.changeClosedMode = this.changeClosedMode.bind(this);
        this.openChooseColor = this.openChooseColor.bind(this);
        this.openChooseLineWeight = this.openChooseLineWeight.bind(this);
        this.handleLineWeightSelect = this.handleLineWeightSelect.bind(this);
        this.handleCloseLineWeight = this.handleCloseLineWeight.bind(this);
        this.closeChooseColor = this.closeChooseColor.bind(this);
        this.updateStrokeColor = this.updateStrokeColor.bind(this);
        this.handleImgChange = this.handleImgChange.bind(this);
      }
    
      /** */
      handleImgChange(newUrl, imgRef) {
        const { image } = this.state;
        this.setState({ image: { ...image, id: newUrl } });
      }
    
      /** */
      handleCloseLineWeight(e) {
        this.setState({
          lineWeightPopoverOpen: false,
          popoverLineWeightAnchorEl: null,
        });
      }
    
      /** */
      handleLineWeightSelect(e) {
        this.setState({
          lineWeightPopoverOpen: false,
          popoverLineWeightAnchorEl: null,
          strokeWidth: e.currentTarget.value,
        });
      }
    
      /** set annotation start time to current time */
      setTstartNow() {
        // eslint-disable-next-line react/destructuring-assignment
        this.setState({ tstart: Math.floor(this.props.currentTime) });
      }
    
      /** set annotation end time to current time */
      setTendNow() {
        // eslint-disable-next-line react/destructuring-assignment
        this.setState({ tend: Math.floor(this.props.currentTime) });
      }
    
      /** seekTo/goto annotation start time */
      seekToTstart() {
        const { paused, setCurrentTime, setSeekTo } = this.props;
        const { tstart } = this.state;
        if (!paused) {
          this.setState(setSeekTo(tstart));
        } else {
          this.setState(setCurrentTime(tstart));
        }
      }
    
      /** seekTo/goto annotation end time */
      seekToTend() {
        const { paused, setCurrentTime, setSeekTo } = this.props;
        const { tend } = this.state;
        if (!paused) {
          this.setState(setSeekTo(tend));
        } else {
          this.setState(setCurrentTime(tend));
        }
      }
    
      /** update annotation start time */
      updateTstart(value) { this.setState({ tstart: value }); }
    
      /** update annotation end time */
      updateTend(value) { this.setState({ tend: value }); }
    
      /** */
      openChooseColor(e) {
        this.setState({
          colorPopoverOpen: true,
          currentColorType: e.currentTarget.value,
          popoverAnchorEl: e.currentTarget,
        });
      }
    
      /** */
      openChooseLineWeight(e) {
        this.setState({
          lineWeightPopoverOpen: true,
          popoverLineWeightAnchorEl: e.currentTarget,
        });
      }
    
      /** */
      closeChooseColor(e) {
        this.setState({
          colorPopoverOpen: false,
          currentColorType: null,
          popoverAnchorEl: null,
        });
      }
    
      /** */
      updateStrokeColor(color) {
        const { currentColorType } = this.state;
        this.setState({
          [currentColorType]: color.hex,
        });
      }
    
      /** */
      submitForm(e) {
        e.preventDefault();
        const {
          annotation, canvases, receiveAnnotation, config,
        } = this.props;
        const {
          textBody, image, tags, xywh, svg, tstart, tend, textEditorStateBustingKey,
        } = this.state;
        const t = (tstart && tend) ? `${tstart},${tend}` : null;
        const body = { value: (!textBody.length && t) ? `${secondsToHMS(tstart)} -> ${secondsToHMS(tend)}` : textBody };
    
        canvases.forEach((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,
          }).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);
            });
          }
        });
    
        this.setState({
          image: { id: null },
          svg: null,
          tend: null,
          textBody: '',
          textEditorStateBustingKey: textEditorStateBustingKey + 1,
          tstart: null,
          xywh: null,
        });
      }
    
      /** */
      changeTool(e, tool) {
        this.setState({
          activeTool: tool,
        });
      }
    
      /** */
      changeClosedMode(e) {
        this.setState({
          closedMode: e.currentTarget.value,
        });
      }
    
      /** */
      updateTextBody(textBody) {
        this.setState({ textBody });
      }
    
      /** */
      updateGeometry({ svg, xywh }) {
        this.setState({
          svg, xywh,
        });
      }
    
      /** */
      render() {
        const {
          annotation, classes, closeCompanionWindow, id, windowId,
        } = this.props;
    
        const {
          activeTool, colorPopoverOpen, currentColorType, fillColor, popoverAnchorEl,
          strokeColor, popoverLineWeightAnchorEl, lineWeightPopoverOpen, strokeWidth, closedMode,
          textBody, svg, tstart, tend,
          textEditorStateBustingKey, image,
        } = this.state;
    
        const mediaIsVideo = typeof VideosReferences.get(windowId) !== 'undefined';
    
        return (
          <CompanionWindow
            title={annotation ? 'Edit annotation' : 'New annotation'}
            windowId={windowId}
            id={id}
          >
            <AnnotationDrawing
              activeTool={activeTool}
              annotation={annotation}
              fillColor={fillColor}
              strokeColor={strokeColor}
              strokeWidth={strokeWidth}
              closed={closedMode === 'closed'}
              svg={svg}
              updateGeometry={this.updateGeometry}
              windowId={windowId}
              player={mediaIsVideo ? VideosReferences.get(windowId) : OSDReferences.get(windowId)}
            />
            <form onSubmit={this.submitForm} className={classes.section}>
              <Grid container>
                <Grid item xs={12}>
                  <Typography variant="overline">
                    Target
                  </Typography>
                </Grid>
                <Grid item xs={12}>
                  <Paper elevation={0} className={classes.paper}>
                    <ToggleButtonGroup
                      className={classes.grouped}
                      value={activeTool}
                      exclusive
                      onChange={this.changeTool}
                      aria-label="tool selection"
                      size="small"
                    >
                      <ToggleButton value="cursor" aria-label="select cursor">
                        <CursorIcon />
                      </ToggleButton>
                      <ToggleButton value="edit" aria-label="select cursor">
                        <FormatShapesIcon />
                      </ToggleButton>
                    </ToggleButtonGroup>
                    <Divider flexItem orientation="vertical" className={classes.divider} />
                    <ToggleButtonGroup
                      className={classes.grouped}
                      value={activeTool}
                      exclusive
                      onChange={this.changeTool}
                      aria-label="tool selection"
                      size="small"
                    >
                      <ToggleButton value="rectangle" aria-label="add a rectangle">
                        <RectangleIcon />
                      </ToggleButton>
                      <ToggleButton value="ellipse" aria-label="add a circle">
                        <CircleIcon />
                      </ToggleButton>
                      <ToggleButton value="polygon" aria-label="add a polygon">
                        <PolygonIcon />
                      </ToggleButton>
                      <ToggleButton value="freehand" aria-label="free hand polygon">
                        <GestureIcon />
                      </ToggleButton>
                    </ToggleButtonGroup>
                  </Paper>
                </Grid>
              </Grid>
              <Grid container>
                <Grid item xs={12}>
                  <Typography variant="overline">
                    Style
                  </Typography>
                </Grid>
                <Grid item xs={12}>
                  <ToggleButtonGroup
                    aria-label="style selection"
                    size="small"
                  >
                    <ToggleButton
                      value="strokeColor"
                      aria-label="select color"
                      onClick={this.openChooseColor}
                    >
                      <StrokeColorIcon style={{ fill: strokeColor }} />
                      <ArrowDropDownIcon />
                    </ToggleButton>
                    <ToggleButton
                      value="strokeColor"
                      aria-label="select line weight"
                      onClick={this.openChooseLineWeight}
                    >
                      <LineWeightIcon />
                      <ArrowDropDownIcon />
                    </ToggleButton>
                    <ToggleButton
                      value="fillColor"
                      aria-label="select color"
                      onClick={this.openChooseColor}
                    >
                      <FormatColorFillIcon style={{ fill: fillColor }} />
                      <ArrowDropDownIcon />
                    </ToggleButton>
                  </ToggleButtonGroup>
    
                  <Divider flexItem orientation="vertical" className={classes.divider} />
                  { /* close / open polygon mode only for freehand drawing mode. */
                    activeTool === 'freehand'
                      ? (
                        <ToggleButtonGroup
                          size="small"
                          value={closedMode}
                          onChange={this.changeClosedMode}
                        >
                          <ToggleButton value="closed">
                            <ClosedPolygonIcon />
                          </ToggleButton>
                          <ToggleButton value="open">
                            <OpenPolygonIcon />
                          </ToggleButton>
                        </ToggleButtonGroup>
                      )
                      : null
                  }
                </Grid>
              </Grid>
              <Grid container>
                { mediaIsVideo && (
                <>
                  <Grid item xs={12}>
                    <ToggleButton value="true" title="Go to start time" size="small" onClick={this.seekToTstart} className={classes.timecontrolsbutton}>
                      <LastPage />
                    </ToggleButton>
                    <Typography variant="overline">
                      Start
                    </Typography>
                  </Grid>
    
                  <Grid item xs={12} className={classes.paper}>
                    <ToggleButton value="true" title="Set current time" size="small" onClick={this.setTstartNow} className={classes.timecontrolsbutton}>
                      <Alarm />
                    </ToggleButton>
                    <HMSInput seconds={tstart} onChange={this.updateTstart} />
                  </Grid>
    
                  <Grid item xs={12}>
                    <Typography variant="overline">
                      <ToggleButton value="true" title="Go to end time" size="small" onClick={this.seekToTend} className={classes.timecontrolsbutton}>
                        <LastPage />
                      </ToggleButton>
                      End
                    </Typography>
                  </Grid>
    
                  <Grid item xs={12} className={classes.paper}>
                    <ToggleButton value="true" title="Set current time" size="small" onClick={this.setTendNow} className={classes.timecontrolsbutton}>
                      <Alarm />
                    </ToggleButton>
                    <HMSInput seconds={tend} onChange={this.updateTend} />
                  </Grid>
                </>
                )}
                <Grid item xs={12}>
                  <Typography variant="overline">
                    Image Content
                  </Typography>
                </Grid>
                <Grid item xs={12} style={{ marginBottom: 10 }}>
                  <ImageFormField value={image} onChange={this.handleImgChange} />
                </Grid>
                <Grid item xs={12}>
                  <Typography variant="overline">
                    Text Content
                  </Typography>
                </Grid>
                <Grid item xs={12}>
                  <TextEditor
                    key={textEditorStateBustingKey}
                    annoHtml={textBody}
                    updateAnnotationBody={this.updateTextBody}
                  />
                </Grid>
              </Grid>
              <Button onClick={closeCompanionWindow}>
                Cancel
              </Button>
              <Button variant="contained" color="primary" type="submit">
                Save
              </Button>
            </form>
            <Popover
              open={lineWeightPopoverOpen}
              anchorEl={popoverLineWeightAnchorEl}
            >
              <Paper>
                <ClickAwayListener onClickAway={this.handleCloseLineWeight}>
                  <MenuList autoFocus role="listbox">
                    {[1, 3, 5, 10, 50].map((option, index) => (
                      <MenuItem
                        key={option}
                        onClick={this.handleLineWeightSelect}
                        value={option}
                        selected={option == strokeWidth}
                        role="option"
                        aria-selected={option == strokeWidth}
                      >
                        {option}
                      </MenuItem>
                    ))}
                  </MenuList>
                </ClickAwayListener>
              </Paper>
            </Popover>
            <Popover
              open={colorPopoverOpen}
              anchorEl={popoverAnchorEl}
              onClose={this.closeChooseColor}
            >
              <SketchPicker
                // eslint-disable-next-line react/destructuring-assignment
                color={this.state[currentColorType] || {}}
                onChangeComplete={this.updateStrokeColor}
              />
            </Popover>
          </CompanionWindow>
        );
      }
    }
    
    /** */
    const styles = (theme) => ({
      divider: {
        margin: theme.spacing(1, 0.5),
      },
      grouped: {
        '&:first-child': {
          borderRadius: theme.shape.borderRadius,
        },
        '&:not(:first-child)': {
          borderRadius: theme.shape.borderRadius,
        },
        border: 'none',
        margin: theme.spacing(0.5),
      },
      paper: {
        display: 'flex',
        flexWrap: 'wrap',
      },
      section: {
        paddingBottom: theme.spacing(1),
        paddingLeft: theme.spacing(2),
        paddingRight: theme.spacing(1),
        paddingTop: theme.spacing(2),
      },
      timecontrolsbutton: {
        height: '30px',
        margin: 'auto',
        marginLeft: '0',
        marginRight: '5px',
        width: '30px',
      },
    });
    
    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 }),
      ),
      classes: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
      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,
      paused: PropTypes.bool,
      receiveAnnotation: PropTypes.func.isRequired,
      setCurrentTime: PropTypes.func,
      setSeekTo: PropTypes.func,
      windowId: PropTypes.string.isRequired,
    };
    
    AnnotationCreation.defaultProps = {
      annotation: null,
      canvases: [],
      closeCompanionWindow: () => {},
      currentTime: null,
      paused: true,
      setCurrentTime: () => {},
      setSeekTo: () => {},
    };
    
    export default withStyles(styles)(AnnotationCreation);