Skip to content
Snippets Groups Projects
Select Git revision
  • 03c13a79c65737a48024e6b94b738fb390a7f34b
  • mui5-annotation-on-video-stable default
  • get_setter_canvasSizeInformations
  • fix-error-div-into-p
  • annotation-on-video-v2
  • detached
  • annotation-on-video-r17
  • mui5
  • mui5-react-18
  • jacob-test
  • annotation-on-video protected
  • master
  • test-antoinev1
  • 20-fetch-thumbnail-on-annotation
  • add-research-field
  • Save
  • add-plugin
  • 14-wip-no-seek-to
  • 14-bug-on-video-time-control
  • 9_wip_videotests
  • _upgrade_material_ui
  • latest-tetras-16
  • v3.3.0
  • v3.2.0
  • v3.1.1
  • v3.1.0
  • v3.0.0
  • v3.0.0-rc.7
  • v3.0.0-rc.6
  • v3.0.0-rc.5
  • v3.0.0-rc.4
  • v3.0.0-rc.3
  • v3.0.0-rc.2
  • v3.0.0-rc.1
  • v3.0.0-beta.10
  • v3.0.0-beta.9
  • v3.0.0-beta.8
  • v3.0.0-beta.7
  • v3.0.0-beta.6
  • v3.0.0-beta.5
  • v3.0.0-beta.3
41 results

setupJest.js

Blame
  • AnnotationsOverlay.js 12.94 KiB
    import React, { Component } from 'react';
    import ReactDOM from 'react-dom';
    import PropTypes from 'prop-types';
    import isEqual from 'lodash/isEqual';
    import debounce from 'lodash/debounce';
    import flatten from 'lodash/flatten';
    import sortBy from 'lodash/sortBy';
    import xor from 'lodash/xor';
    import OpenSeadragonCanvasOverlay from '../lib/OpenSeadragonCanvasOverlay';
    import CanvasWorld from '../lib/CanvasWorld';
    import CanvasAnnotationDisplay from '../lib/CanvasAnnotationDisplay';
    
    /**
     * Represents a OpenSeadragonViewer in the mirador workspace. Responsible for mounting
     * and rendering OSD.
     */
    export class AnnotationsOverlay extends Component {
      /**
       * annotationsMatch - compares previous annotations to current to determine
       * whether to add a new updateCanvas method to draw annotations
       * @param  {Array} currentAnnotations
       * @param  {Array} prevAnnotations
       * @return {Boolean}
       */
      static annotationsMatch(currentAnnotations, prevAnnotations) {
        if (!currentAnnotations && !prevAnnotations) return true;
        if (
          (currentAnnotations && !prevAnnotations)
          || (!currentAnnotations && prevAnnotations)
        ) return false;
    
        if (currentAnnotations.length === 0 && prevAnnotations.length === 0) return true;
        if (currentAnnotations.length !== prevAnnotations.length) return false;
        return currentAnnotations.every((annotation, index) => {
          const newIds = annotation.resources.map(r => r.id);
          const prevIds = prevAnnotations[index].resources.map(r => r.id);
          if (newIds.length === 0 && prevIds.length === 0) return true;
          if (newIds.length !== prevIds.length) return false;
    
          if ((annotation.id === prevAnnotations[index].id) && (isEqual(newIds, prevIds))) {
            return true;
          }
          return false;
        });
      }
    
      /**
       * @param {Object} props
       */
      constructor(props) {
        super(props);
    
        this.ref = React.createRef();
        this.osdCanvasOverlay = null;
        // An initial value for the updateCanvas method
        this.updateCanvas = () => {};
        this.onUpdateViewport = this.onUpdateViewport.bind(this);
        this.onCanvasClick = this.onCanvasClick.bind(this);
        this.onCanvasMouseMove = debounce(this.onCanvasMouseMove.bind(this), 10);
        this.onCanvasExit = this.onCanvasExit.bind(this);
      }
    
      /**
       * React lifecycle event
       */
      componentDidMount() {
        this.initializeViewer();
      }
    
      /**
       * When the tileSources change, make sure to close the OSD viewer.
       * When the annotations change, reset the updateCanvas method to make sure
       * they are added.
       * When the viewport state changes, pan or zoom the OSD viewer as appropriate
       */
      componentDidUpdate(prevProps) {
        const {
          drawAnnotations,
          drawSearchAnnotations,
          annotations, searchAnnotations,
          hoveredAnnotationIds, selectedAnnotationId,
          highlightAllAnnotations,
          viewer,
        } = this.props;
    
        this.initializeViewer();
    
        const annotationsUpdated = !AnnotationsOverlay.annotationsMatch(annotations, prevProps.annotations);
        const searchAnnotationsUpdated = !AnnotationsOverlay.annotationsMatch(
          searchAnnotations,
          prevProps.searchAnnotations,
        );
    
        const hoveredAnnotationsUpdated = (
          xor(hoveredAnnotationIds, prevProps.hoveredAnnotationIds).length > 0
        );
    
        if (this.osdCanvasOverlay && hoveredAnnotationsUpdated) {
          if (hoveredAnnotationIds.length > 0) {
            this.osdCanvasOverlay.canvasDiv.style.cursor = 'pointer';
          } else {
            this.osdCanvasOverlay.canvasDiv.style.cursor = '';
          }
        }
    
        const selectedAnnotationsUpdated = selectedAnnotationId !== prevProps.selectedAnnotationId;
    
        const redrawAnnotations = drawAnnotations !== prevProps.drawAnnotations
          || drawSearchAnnotations !== prevProps.drawSearchAnnotations
          || highlightAllAnnotations !== prevProps.highlightAllAnnotations;
    
        if (
          searchAnnotationsUpdated
          || annotationsUpdated
          || selectedAnnotationsUpdated
          || hoveredAnnotationsUpdated
          || redrawAnnotations
        ) {
          this.updateCanvas = this.canvasUpdateCallback();
          viewer.forceRedraw();
        }
      }
    
      /**
       */
      componentWillUnmount() {
        const { viewer } = this.props;
    
        viewer.removeHandler('canvas-click', this.onCanvasClick);
        viewer.removeHandler('canvas-exit', this.onCanvasExit);
        viewer.removeHandler('update-viewport', this.onUpdateViewport);
        viewer.removeHandler('mouse-move', this.onCanvasMouseMove);
      }
    
      /** */
      onCanvasClick(event) {
        const {
          canvasWorld,
        } = this.props;
    
        const { position: webPosition, eventSource: { viewport } } = event;
        const point = viewport.pointFromPixel(webPosition);
    
        const canvas = canvasWorld.canvasAtPoint(point);
        if (!canvas) return;
        const [
          _canvasX, _canvasY, canvasWidth, canvasHeight, // eslint-disable-line no-unused-vars
        ] = canvasWorld.canvasToWorldCoordinates(canvas.id);
    
        // get all the annotations that contain the click
        const annos = this.annotationsAtPoint(canvas, point);
    
        if (annos.length > 0) {
          event.preventDefaultAction = true; // eslint-disable-line no-param-reassign
        }
    
        if (annos.length === 1) {
          this.toggleAnnotation(annos[0].id);
        } else if (annos.length > 0) {
          /**
           * Try to find the "right" annotation to select after a click.
           *
           * This is perhaps a naive method, but seems to deal with rectangles and SVG shapes:
           *
           * - figure out how many points around a circle are inside the annotation shape
           * - if there's a shape with the fewest interior points, it's probably the one
           *       with the closest boundary?
           * - if there's a tie, make the circle bigger and try again.
           */
          const annosWithClickScore = (radius) => {
            const degreesToRadians = Math.PI / 180;
    
            return (anno) => {
              let score = 0;
              for (let degrees = 0; degrees < 360; degrees += 1) {
                const x = Math.cos(degrees * degreesToRadians) * radius + point.x;
                const y = Math.sin(degrees * degreesToRadians) * radius + point.y;
    
                if (this.isAnnotationAtPoint(anno, canvas, { x, y })) score += 1;
              }
    
              return { anno, score };
            };
          };
    
          let annosWithScore = [];
          let radius = 1;
          annosWithScore = sortBy(annos.map(annosWithClickScore(radius)), 'score');
    
          while (radius < Math.max(canvasWidth, canvasHeight)
            && annosWithScore[0].score === annosWithScore[1].score) {
            radius *= 2;
            annosWithScore = sortBy(annos.map(annosWithClickScore(radius)), 'score');
          }
    
          this.toggleAnnotation(annosWithScore[0].anno.id);
        }
      }
    
      /** */
      onCanvasMouseMove(event) {
        const {
          annotations,
          canvasWorld,
          hoverAnnotation,
          hoveredAnnotationIds,
          searchAnnotations,
          viewer,
          windowId,
        } = this.props;
    
        if (annotations.length === 0 && searchAnnotations.length === 0) return;
    
        const { position: webPosition } = event;
        const point = viewer.viewport.pointFromPixel(webPosition);
    
        const canvas = canvasWorld.canvasAtPoint(point);
        if (!canvas) {
          hoverAnnotation(windowId, []);
          return;
        }
    
        const annos = this.annotationsAtPoint(canvas, point);
    
        if (xor(hoveredAnnotationIds, annos.map(a => a.id)).length > 0) {
          hoverAnnotation(windowId, annos.map(a => a.id));
        }
      }
    
      /** If the cursor leaves the canvas, wipe out highlights */
      onCanvasExit(event) {
        const {
          hoverAnnotation,
          windowId,
        } = this.props;
    
        // a move event may be queued up by the debouncer
        this.onCanvasMouseMove.cancel();
        hoverAnnotation(windowId, []);
      }
    
      /**
       * onUpdateViewport - fires during OpenSeadragon render method.
       */
      onUpdateViewport(event) {
        this.updateCanvas();
      }
    
      /** @private */
      initializeViewer() {
        const { viewer } = this.props;
    
        if (!viewer) return;
        if (this.osdCanvasOverlay) return;
    
        this.osdCanvasOverlay = new OpenSeadragonCanvasOverlay(viewer, this.ref);
    
        viewer.addHandler('canvas-click', this.onCanvasClick);
        viewer.addHandler('canvas-exit', this.onCanvasExit);
        viewer.addHandler('update-viewport', this.onUpdateViewport);
        viewer.addHandler('mouse-move', this.onCanvasMouseMove);
    
        this.updateCanvas = this.canvasUpdateCallback();
      }
    
      /** */
      canvasUpdateCallback() {
        return () => {
          this.osdCanvasOverlay.clear();
          this.osdCanvasOverlay.resize();
          this.osdCanvasOverlay.canvasUpdate(this.renderAnnotations.bind(this));
        };
      }
    
      /** @private */
      isAnnotationAtPoint(resource, canvas, point) {
        const {
          canvasWorld,
        } = this.props;
    
        const [canvasX, canvasY] = canvasWorld.canvasToWorldCoordinates(canvas.id);
        const relativeX = point.x - canvasX;
        const relativeY = point.y - canvasY;
    
        if (resource.svgSelector) {
          const context = this.osdCanvasOverlay.context2d;
          const { svgPaths } = new CanvasAnnotationDisplay({ resource });
          return [...svgPaths].some(path => (
            context.isPointInPath(new Path2D(path.attributes.d.nodeValue), relativeX, relativeY)
          ));
        }
    
        if (resource.fragmentSelector) {
          const [x, y, w, h] = resource.fragmentSelector;
          return x <= relativeX && relativeX <= (x + w)
            && y <= relativeY && relativeY <= (y + h);
        }
        return false;
      }
    
      /** @private */
      annotationsAtPoint(canvas, point) {
        const {
          annotations, searchAnnotations,
        } = this.props;
    
        const lists = [...annotations, ...searchAnnotations];
        const annos = flatten(lists.map(l => l.resources)).filter((resource) => {
          if (canvas.id !== resource.targetId) return false;
    
          return this.isAnnotationAtPoint(resource, canvas, point);
        });
    
        return annos;
      }
    
      /** */
      toggleAnnotation(id) {
        const {
          selectedAnnotationId,
          selectAnnotation,
          deselectAnnotation,
          windowId,
        } = this.props;
    
        if (selectedAnnotationId === id) {
          deselectAnnotation(windowId, id);
        } else {
          selectAnnotation(windowId, id);
        }
      }
    
      /**
       * annotationsToContext - converts anontations to a canvas context
       */
      annotationsToContext(annotations, palette) {
        const {
          highlightAllAnnotations, hoveredAnnotationIds, selectedAnnotationId, canvasWorld,
          viewer,
        } = this.props;
        const context = this.osdCanvasOverlay.context2d;
        const zoomRatio = viewer.viewport.getZoom(true) / viewer.viewport.getMaxZoom();
        annotations.forEach((annotation) => {
          annotation.resources.forEach((resource) => {
            if (!canvasWorld.canvasIds.includes(resource.targetId)) return;
            const offset = canvasWorld.offsetByCanvas(resource.targetId);
            const canvasAnnotationDisplay = new CanvasAnnotationDisplay({
              hovered: hoveredAnnotationIds.includes(resource.id),
              offset,
              palette: {
                ...palette,
                default: {
                  ...palette.default,
                  ...(!highlightAllAnnotations && palette.hidden),
                },
              },
              resource,
              selected: selectedAnnotationId === resource.id,
              zoomRatio,
            });
            canvasAnnotationDisplay.toContext(context);
          });
        });
      }
    
      /** */
      renderAnnotations() {
        const {
          annotations,
          drawAnnotations,
          drawSearchAnnotations,
          searchAnnotations,
          palette,
        } = this.props;
    
        if (drawSearchAnnotations) {
          this.annotationsToContext(searchAnnotations, palette.search);
        }
    
        if (drawAnnotations) {
          this.annotationsToContext(annotations, palette.annotations);
        }
      }
    
      /**
       * Renders things
       */
      render() {
        const { viewer } = this.props;
    
        if (!viewer) return <></>;
    
        return ReactDOM.createPortal(
          (
            <div
              ref={this.ref}
              style={{
                height: '100%', left: 0, position: 'absolute', top: 0, width: '100%',
              }}
            >
              <canvas />
            </div>
          ),
          viewer.canvas,
        );
      }
    }
    
    AnnotationsOverlay.defaultProps = {
      annotations: [],
      deselectAnnotation: () => {},
      drawAnnotations: true,
      drawSearchAnnotations: true,
      highlightAllAnnotations: false,
      hoverAnnotation: () => {},
      hoveredAnnotationIds: [],
      palette: {},
      searchAnnotations: [],
      selectAnnotation: () => {},
      selectedAnnotationId: undefined,
      viewer: null,
    };
    
    AnnotationsOverlay.propTypes = {
      annotations: PropTypes.arrayOf(PropTypes.object), // eslint-disable-line react/forbid-prop-types
      canvasWorld: PropTypes.instanceOf(CanvasWorld).isRequired,
      deselectAnnotation: PropTypes.func,
      drawAnnotations: PropTypes.bool,
      drawSearchAnnotations: PropTypes.bool,
      highlightAllAnnotations: PropTypes.bool,
      hoverAnnotation: PropTypes.func,
      hoveredAnnotationIds: PropTypes.arrayOf(PropTypes.string),
      palette: PropTypes.object, // eslint-disable-line react/forbid-prop-types
      searchAnnotations: PropTypes.arrayOf(PropTypes.object), // eslint-disable-line react/forbid-prop-types
      selectAnnotation: PropTypes.func,
      selectedAnnotationId: PropTypes.string,
      viewer: PropTypes.object, // eslint-disable-line react/forbid-prop-types
      windowId: PropTypes.string.isRequired,
    };