import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import ResizeObserver from 'react-resize-observer'; import { OSDReferences } from 'mirador/dist/es/src/plugins/OSDReferences'; import { VideosReferences } from 'mirador/dist/es/src/plugins/VideosReferences'; import { renderWithPaperScope, PaperContainer } from '@psychobolt/react-paperjs'; import { EllipseTool, PolygonTool, RectangleTool, FreeformPathTool, } from '@psychobolt/react-paperjs-editor'; import { Point } from 'paper'; import flatten from 'lodash/flatten'; import EditTool from './EditTool'; import { mapChildren } from './utils'; /** Create a portal with a drawing canvas and a form to fill annotations details */ class AnnotationDrawing extends Component { /** */ constructor(props) { super(props); this.paper = null; this.getDisplayProps = this.getDisplayProps.bind(this); this.onPaperResize = this.onPaperResize.bind(this); this.paperDidMount = this.paperDidMount.bind(this); this.addPath = this.addPath.bind(this); } /** Sync drawing canvas on componentDidMount */ componentDidMount() { this.onPaperResize(); } /** Sync drawing canvas on componentDidUpdate */ componentDidUpdate() { this.onPaperResize(); } /** Sync drawing canvas size/zoom with annotations canvas */ onPaperResize(ev) { const { windowId } = this.props; if (VideosReferences.get(windowId) && this.paper) { const { canvasOverlay, video } = VideosReferences.get(windowId); const { height, width } = canvasOverlay.ref.current; const { videoHeight, videoWidth } = video; this.paper.view.center = new Point(videoWidth / 2, videoHeight / 2); this.paper.view.zoom = canvasOverlay.scale; this.paper.view.viewSize = new this.paper.Size(width, height); } } /** Build parameters to paperjs View and canvas */ getDisplayProps() { const { windowId } = this.props; const osdref = OSDReferences.get(windowId); const videoref = VideosReferences.get(windowId); if (osdref && videoref) { console.error('Unhandled case: both OpenSeadragon (picture viewer) and video player on the same canvas'); } if (osdref) { const { viewport } = osdref.current; const img = osdref.current.world.getItemAt(0); const center = img.viewportToImageCoordinates(viewport.getCenter(true)); return { canvasProps: { style: { height: '100%', width: '100%' } }, viewProps: { center: new Point(center.x, center.y), rotation: viewport.getRotation(), scaling: new Point(viewport.getFlip() ? -1 : 1, 1), zoom: img.viewportToImageZoom(viewport.getZoom()), }, }; } if (videoref) { const { height, width } = videoref.canvasOverlay.ref.current; return { canvasProps: { height, resize: 'true', style: { left: 0, position: 'absolute', top: 0, }, width, }, viewProps: { center: new Point(width / 2, height / 2), height, width, zoom: videoref.canvasOverlay.scale, }, }; } throw new Error('Unknown or missing data player, not OpenSeadragon (picture viewer) nor the video player'); } /** Draw SVG on canvas */ addPath(path) { const { closed, strokeWidth, updateGeometry } = this.props; // TODO: Compute xywh of bounding container of layers const { bounds } = path; const { x, y, width, height, } = bounds; path.closed = closed; // eslint-disable-line no-param-reassign // Reset strokeWidth for persistence path.strokeWidth = strokeWidth; // eslint-disable-line no-param-reassign path.data.state = null; // eslint-disable-line no-param-reassign const svgExports = flatten(path.project.layers.map((layer) => ( flatten(mapChildren(layer)).map((aPath) => aPath.exportSVG({ asString: true })) ))); svgExports.unshift("<svg xmlns='http://www.w3.org/2000/svg'>"); svgExports.push('</svg>'); updateGeometry({ svg: svgExports.join(''), xywh: [ Math.floor(x), Math.floor(y), Math.floor(width), Math.floor(height), ].join(','), }); } /** Save paperjs ref once created */ paperDidMount(paper) { this.paper = paper; } /** */ paperThing() { const { viewProps, canvasProps } = this.getDisplayProps(); const { activeTool, fillColor, strokeColor, strokeWidth, svg, } = this.props; if (!activeTool || activeTool === 'cursor') return null; let ActiveTool = RectangleTool; switch (activeTool) { case 'rectangle': ActiveTool = RectangleTool; break; case 'ellipse': ActiveTool = EllipseTool; break; case 'polygon': ActiveTool = PolygonTool; break; case 'freehand': ActiveTool = FreeformPathTool; break; case 'edit': ActiveTool = EditTool; break; default: break; } return ( <div style={{ height: '100%', left: 0, position: 'absolute', top: 0, width: '100%', }} > <PaperContainer canvasProps={canvasProps} viewProps={viewProps} onMount={this.paperDidMount} > {renderWithPaperScope((paper) => { const paths = flatten(paper.project.layers.map((layer) => ( flatten(mapChildren(layer)).map((aPath) => aPath) ))); if (svg && paths.length === 0) { paper.project.importSVG(svg); } paper.settings.handleSize = 10; // eslint-disable-line no-param-reassign paper.settings.hitTolerance = 10; // eslint-disable-line no-param-reassign return ( <ActiveTool onPathAdd={this.addPath} pathProps={{ fillColor, strokeColor, strokeWidth: strokeWidth / paper.view.zoom, }} paper={paper} /> ); })} </PaperContainer> <ResizeObserver onResize={this.onPaperResize} /> </div> ); } /** */ render() { const { windowId } = this.props; const osdref = OSDReferences.get(windowId); const videoref = VideosReferences.get(windowId); if (!osdref && !videoref) { throw new Error("Unknown or missing data player, didn't found OpenSeadragon (image viewer) nor the video player"); } const container = osdref ? osdref.current.element : videoref.ref.current.parentElement; return ( ReactDOM.createPortal(this.paperThing(), container) ); } } AnnotationDrawing.propTypes = { activeTool: PropTypes.string, closed: PropTypes.bool, fillColor: PropTypes.string, strokeColor: PropTypes.string, strokeWidth: PropTypes.number, svg: PropTypes.string, updateGeometry: PropTypes.func.isRequired, windowId: PropTypes.string.isRequired, }; AnnotationDrawing.defaultProps = { activeTool: null, closed: true, fillColor: null, strokeColor: '#00BFFF', strokeWidth: 1, svg: null, }; export default AnnotationDrawing;