diff --git a/src/AnnotationCreation.js b/src/AnnotationCreation.js index 0b97a01c781123c7bd8fa604ad516bcec7e52059..6fea483952ba4a92bba688f2c189bdbb96b2bb92 100644 --- a/src/AnnotationCreation.js +++ b/src/AnnotationCreation.js @@ -1,11 +1,6 @@ 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'; @@ -16,13 +11,13 @@ 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 { secondsToHMS } from './utils'; import AnnotationFormContent from './annotationForm/AnnotationFormContent'; import AnnotationFormTime from './annotationForm/AnnotationFormTime'; import { - geomFromAnnoTarget, getJPG, getSvg, saveAnnotation, timeFromAnnoTarget, + geomFromAnnoTarget, timeFromAnnoTarget, } from './AnnotationCreationUtils'; -import AnnotationFormOverlay from './annotationForm/AnnotationFormOverlay/AnnotationFormOverlay.js'; +import AnnotationFormOverlay from './annotationForm/AnnotationFormOverlay/AnnotationFormOverlay'; +import AnnotationFormFooter from './annotationForm/AnnotationFormFooter'; const TARGET_VIEW = 'target'; const OVERLAY_VIEW = 'layer'; @@ -302,54 +297,14 @@ function AnnotationCreation(props) { } }; - /** - * Validate form and save annotation - */ - const submitForm = async (e) => { - console.log('submitForm'); - 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 (viewTool === OVERLAY_VIEW && state.activeTool === 'edit') { - setState((prevState) => ({ - ...prevState, - activeTool: 'cursor', - })); - return; - } */ - - const { - annotation, - canvases, - receiveAnnotation, - config, - } = props; - - - const drawingStateSerialized = JSON.stringify(drawingState); - - const { - textBody, - tags, - xywh, - tstart, - tend, - image, - } = state; - // TODO rename variable for better comprenhension - const svg = await getSvg(props.windowId); - // const jpg = await getJPG(props.windowId); - const drawingImageExport = svg; - const t = (tstart && tend) ? `${tstart},${tend}` : null; - const body = { value: (!textBody.length && t) ? `${secondsToHMS(tstart)} -> ${secondsToHMS(tend)}` : textBody }; - saveAnnotation(canvases, config, receiveAnnotation, annotation, body, t, xywh, image, drawingStateSerialized, drawingImageExport, tags); - - props.closeCompanionWindow('annotationCreation', { - id, + const closeFormCompanionWindow = () => { + closeCompanionWindow('annotationCreation', { + id: props.id, position: 'right', }); + }; + const resetStateAfterSave = () => { // TODO this create a re-render too soon for react and crash the app setState({ image: { id: null }, @@ -360,7 +315,7 @@ function AnnotationCreation(props) { tstart: 0, xywh: null, }); - }; + } /** */ const { @@ -432,7 +387,7 @@ function AnnotationCreation(props) { 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 + // 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} @@ -447,7 +402,6 @@ function AnnotationCreation(props) { setDrawingState={setDrawingState} /> <StyledForm - onSubmit={submitForm} > <TabContext value={viewTool}> <TabList value={viewTool} onChange={tabHandler} aria-label="icon tabs"> @@ -515,30 +469,22 @@ function AnnotationCreation(props) { value={MANIFEST_LINK_VIEW} /> </TabContext> - <StyledButtonDivSaveOrCancel> - <Button onClick={closeCompanionWindow}> - Cancel - </Button> - <Button - variant="contained" - color="primary" - type="submit" - onMouseOver={() => setIsMouseOverSave(true)} - onMouseOut={() => setIsMouseOverSave(false)} - > - Save - </Button> - </StyledButtonDivSaveOrCancel> + <AnnotationFormFooter + annotation={annotation} + canvases={props.canvases} + closeFormCompanionWindow={closeFormCompanionWindow} + config={props.config} + drawingState={drawingState} + receiveAnnotation={props.receiveAnnotation} + resetStateAfterSave={resetStateAfterSave} + state={state} + windowId={windowId} + /> </StyledForm> </CompanionWindow> ); } -const StyledButtonDivSaveOrCancel = styled('div')(({ theme }) => ({ - display: 'flex', - justifyContent: 'flex-end', -})); - const StyledForm = styled('form')(({ theme }) => ({ display: 'flex', flexDirection: 'column', diff --git a/src/AnnotationCreationUtils.js b/src/AnnotationCreationUtils.js index 669cb51c345dfa485765a1c4d986d9aec502aa02..ba25f0e6d8b245234ae4512f15b3f569982d6d53 100644 --- a/src/AnnotationCreationUtils.js +++ b/src/AnnotationCreationUtils.js @@ -1,10 +1,7 @@ -import { exportStageSVG } from 'react-konva-to-svg'; -import { v4 as uuid } from 'uuid'; import axios from 'axios'; -import WebAnnotation from './WebAnnotation'; -const fileUploaderUrl = 'https://scene-uploads.tetras-libre.fr/upload'; -const fileReaderUrl = 'https://scene-uploads.tetras-libre.fr/static/'; +export const fileUploaderUrl = 'https://scene-uploads.tetras-libre.fr/upload'; +export const fileReaderUrl = 'https://scene-uploads.tetras-libre.fr/static/'; /*const fileUploaderUrl = 'http://localhost:3000/upload'; const fileReaderUrl = 'http://localhost:3000/static/';*/ @@ -52,55 +49,28 @@ export function isShapesTool(activeTool) { return Object.values(SHAPES_TOOL).find((tool) => tool === activeTool); } -/** - * Get SVG picture containing all the stuff draw in the stage (Konva Stage). - * This image will be put in overlay of the iiif media - */ -export async function getSvg(windowId) { - const stage = window.Konva.stages.find((s) => s.attrs.id === windowId); - const svg = await exportStageSVG(stage, false); // TODO clean - console.log('SVG:', svg); - return svg; -} - -export async function getJPG(windowId) { - const stage = window.Konva.stages.find((s) => s.attrs.id === windowId); - const jpg = await stage.toImage({ mimeType: 'image/jpeg', quality: 1 }); - console.log('JPG:', jpg); - return jpg; +/** Save annotation in the storage adapter */ +export async function saveAnnotation(canvas, storageAdapter, receiveAnnotation, annotationToSaved, isNewAnnotation) { + if (isNewAnnotation) { + storageAdapter.update(annotationToSaved) + .then((annoPage) => { + receiveAnnotation(canvas.id, storageAdapter.annotationPageId, annoPage); + }); + } else { + storageAdapter.create(annotationToSaved) + .then((annoPage) => { + receiveAnnotation(canvas.id, storageAdapter.annotationPageId, annoPage); + }); + } } -export async function saveAnnotation(canvases, config, receiveAnnotation, annotation, body, t, xywh, image, drawingStateSerialized, drawingImageExport, tags) { - console.log('Send file :', drawingImageExport); - const filename = await sendFile(drawingImageExport); - +export async function saveAnnotationInEachCanvas(canvases, config, receiveAnnotation, annotationToSaved, target, isNewAnnotation) { canvases.forEach(async (canvas) => { + // Adapt target to the canvas + // eslint-disable-next-line no-param-reassign + annotationToSaved.target = `${canvas.id}#xywh=${target.xywh}&t=${target.t}`; const storageAdapter = config.annotation.adapter(canvas.id); - const anno = { - body: { - id: fileReaderUrl + filename, - type: 'Image', - format: 'image/svg+xml', - value: body.value, - }, - drawingState: drawingStateSerialized, - id: (annotation && annotation.id) || `${uuid()}`, - motivation: 'commenting', - target: `${canvas.id}#xywh=${xywh}&t=${t}`, - type: 'Annotation', - }; - - 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); - }); - } + saveAnnotation(canvas, storageAdapter, receiveAnnotation, annotationToSaved, isNewAnnotation); }); } @@ -110,7 +80,6 @@ const sendFile = async (fileContent) => { const formData = new FormData(); formData.append('file', blob); - try { const response = await axios.post(fileUploaderUrl, formData, { headers: { @@ -128,20 +97,11 @@ const sendFile = async (fileContent) => { }; -// export function dataURLtoBlob(dataurl) { -// // Split the Data URL to get the metadata and the actual data -// console.log('Data URL:', dataurl); -// var arr = dataurl.split(','), -// mime = arr[0].match(/:(.*?);/)[1], // Extract MIME type -// bstr = atob(arr[1]), // Decode base64 -// n = bstr.length, -// u8arr = new Uint8Array(n); // Create a new ArrayBuffer -// -// // Convert the binary string to an ArrayBuffer -// while(n--){ -// u8arr[n] = bstr.charCodeAt(n); -// } -// -// // Return a Blob object -// return new Blob([u8arr], {type:mime}); +function dataURLtoBlob(dataurl) { + var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1], + bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); + while(n--){ + u8arr[n] = bstr.charCodeAt(n); + } + return new Blob([u8arr], {type:mime}); } diff --git a/src/annotationForm/AnnotationFormFooter.js b/src/annotationForm/AnnotationFormFooter.js new file mode 100644 index 0000000000000000000000000000000000000000..67d33a24556b03d84bcd8a1c9ce109f89479cdeb --- /dev/null +++ b/src/annotationForm/AnnotationFormFooter.js @@ -0,0 +1,132 @@ +import { Button } from '@mui/material'; +import PropTypes from 'prop-types'; +import { styled } from '@mui/material/styles'; +import React from 'react'; +import { v4 as uuid } from 'uuid'; +import { + saveAnnotationInEachCanvas, +} from '../AnnotationCreationUtils'; +import { secondsToHMS } from '../utils'; +import { + getJPGAsDataURL, + getKonvaAsDataURL +} from './AnnotationFormOverlay/KonvaDrawing/KonvaUtils'; + +const StyledButtonDivSaveOrCancel = styled('div')(({ theme }) => ({ + display: 'flex', + justifyContent: 'flex-end', +})); + +/** Annotation form footer, save or cancel the edition/creation of an annotation */ +function AnnotationFormFooter({ + annotation, + canvases, + closeFormCompanionWindow, + config, + drawingState, + receiveAnnotation, + resetStateAfterSave, + state, + windowId, +}) { + /** + * Validate form and save annotation + */ + const submitAnnotationForm = async (e) => { + console.log('submitForm'); + 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 (viewTool === OVERLAY_VIEW && state.activeTool === 'edit') { + setState((prevState) => ({ + ...prevState, + activeTool: 'cursor', + })); + return; + } */ + + const { + textBody, + tags, + xywh, + tstart, + tend, + image, + } = state; + + // Save annotation drawing in svg and sent it to the server + // const svg = await getSvg(windowId); + // const drawingImageExport = jpg; + // const filename = await sendFile(drawingImageExport); + // const annotationBodyImageId = fileReaderUrl + filename; + + // Save jpg image of the drawing in a data url + const annotationBodyImageId = getKonvaAsDataURL(windowId); + + // Temporal target of the annotation + const target = { + t: (tstart && tend) ? `${tstart},${tend}` : null, + xywh, // TODO retrouver calcul de xywh + }; + + const annotationText = (!textBody.length && target.t) ? `${secondsToHMS(tstart)} -> ${secondsToHMS(tend)}` : textBody; + + console.log('annotationBodyImageId:', annotationBodyImageId); + + const annotationToSaved = { + body: { + id: annotationBodyImageId, + type: 'Image', + format: 'image/svg+xml', + value: annotationText, + }, + drawingState: JSON.stringify(drawingState), + id: (annotation && annotation.id) || `${uuid()}`, + motivation: 'commenting', + target: null, + type: 'Annotation', // Will be updated in saveAnnotationInEachCanvas + }; + + console.log('Annotation to save:', annotationToSaved); + console.log('target:', target); + + const isNewAnnotation = !annotation; + + saveAnnotationInEachCanvas(canvases, config, receiveAnnotation, annotationToSaved, target, isNewAnnotation); + + closeFormCompanionWindow(); + + resetStateAfterSave(); + }; + + return ( + <StyledButtonDivSaveOrCancel> + <Button onClick={closeFormCompanionWindow}> + Cancel + </Button> + <Button + variant="contained" + color="primary" + type="submit" + onClick={submitAnnotationForm} + > + Save + </Button> + </StyledButtonDivSaveOrCancel> + ); +} + +AnnotationFormFooter.propTypes = { + annotation: PropTypes.object, // eslint-disable-line react/forbid-prop-types + canvases: PropTypes.arrayOf(PropTypes.object).isRequired, + closeFormCompanionWindow: PropTypes.func.isRequired, + config: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + drawingState: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + receiveAnnotation: PropTypes.func.isRequired, + resetStateAfterSave: PropTypes.func.isRequired, + state: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + windowId: PropTypes.string.isRequired, +}; + +export default AnnotationFormFooter; diff --git a/src/annotationForm/AnnotationFormOverlay/KonvaDrawing/KonvaUtils.js b/src/annotationForm/AnnotationFormOverlay/KonvaDrawing/KonvaUtils.js new file mode 100644 index 0000000000000000000000000000000000000000..649f08c97584324793049994cb5d3678b5f68a85 --- /dev/null +++ b/src/annotationForm/AnnotationFormOverlay/KonvaDrawing/KonvaUtils.js @@ -0,0 +1,20 @@ +import { exportStageSVG } from 'react-konva-to-svg'; + +/** + * Get SVG picture containing all the stuff draw in the stage (Konva Stage). + * This image will be put in overlay of the iiif media + */ +export async function getSvg(windowId) { + const stage = window.Konva.stages.find((s) => s.attrs.id === windowId); + const svg = await exportStageSVG(stage, false); // TODO clean + console.log('SVG:', svg); + return svg; +} + +/** Export the stage as a JPG image in a data url */ +export async function getKonvaAsDataURL(windowId) { + const stage = window.Konva.stages.find((s) => s.attrs.id === windowId); + const dataURL = await stage.toDataURL({ mimeType: 'image/png', quality: 1 }); + console.log('dataURL:', dataURL); + return dataURL; +}