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 { 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 { 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 { /** */ static checkURL(url) { const expression = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/; const regex = new RegExp(expression); return url.match(regex); } /** */ static loadImg(url) { return new Promise((resolve, reject) => { const img = new Image(); img.src = url; img.onload = () => resolve({ height: img.height, width: img.width }); img.onerror = reject; }); } /** */ constructor(props) { super(props); const annoState = {}; annoState.image = false; console.log('edition/creation annotation: ', props.annotation); 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; annoState.image = body.image; } }); } 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; annoState.image = props.annotation.body.image; } // // 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', 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, textBody: '', colorPopoverOpen: false, lineWeightPopoverOpen: false, openAddImgDialog: false, popoverAnchorEl: null, popoverLineWeightAnchorEl: null, svg: null, textEditorStateBustingKey: 0, imgConstrain: false, imgHeight: { lastSubmittedValue: '', srcValue: '', validity: 0, value: '', }, imgUrl: { lastSubmittedValue: '', validity: 0, value: '', }, imgWidth: { lastSubmittedValue: '', srcValue: '', validity: 0, value: '', }, xywh: null, ...annoState, }; this.submitForm = this.submitForm.bind(this); // this.updateBody = this.updateBody.bind(this); this.updateTextBody = this.updateTextBody.bind(this); this.getImgDimensions = this.getImgDimensions.bind(this); this.setImgWidth = this.setImgWidth.bind(this); this.setImgHeight = this.setImgHeight.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.isConstrained = this.isConstrained.bind(this); this.handleConstrainCheck = this.handleConstrainCheck.bind(this); this.handleImgDialogChange = this.handleImgDialogChange.bind(this); this.handleImgDialogSubmit = this.handleImgDialogSubmit.bind(this); } /** */ handleImgDialogChange(open) { const { imgHeight, imgWidth, imgUrl } = this.state; this.setState({ imgHeight: { ...imgHeight, validity: 1, value: imgHeight.lastSubmittedValue, }, imgUrl: { ...imgUrl, validity: 1, value: imgUrl.lastSubmittedValue, }, imgWidth: { ...imgWidth, validity: 1, value: imgWidth.lastSubmittedValue, }, openAddImgDialog: open, }); } /** */ handleConstrainCheck(event) { const value = event.target.checked; this.setState({ imgConstrain: value, }); } /** */ handleImgDialogSubmit() { let open = true; const { imgUrl, imgHeight, imgWidth } = this.state; const urlValidity = AnnotationCreation.checkURL(imgUrl.value) ? 1 : 2; const widthValidity = imgWidth.value > 0 ? 1 : 2; const heightValidity = imgHeight.value > 0 ? 1 : 2; if (urlValidity === 1 && widthValidity === 1 && heightValidity === 1) { open = false; } this.setState({ imgHeight: { ...imgHeight, lastSubmittedValue: heightValidity === 1 ? imgHeight.value : imgHeight.lastSubmittedValue, validity: heightValidity, value: imgHeight.value, }, imgUrl: { ...imgUrl, lastSubmittedValue: urlValidity === 1 ? imgUrl.value : imgUrl.lastSubmittedValue, validity: urlValidity, value: imgUrl.value, }, imgWidth: { ...imgWidth, lastSubmittedValue: widthValidity === 1 ? imgWidth.value : imgWidth.lastSubmittedValue, validity: widthValidity, value: imgWidth.value, }, openAddImgDialog: open, }); } /** */ handleCloseLineWeight(e) { this.setState({ lineWeightPopoverOpen: false, popoverLineWeightAnchorEl: null, }); } /** */ handleLineWeightSelect(e) { this.setState({ lineWeightPopoverOpen: false, popoverLineWeightAnchorEl: null, strokeWidth: e.currentTarget.value, }); } /** */ async getImgDimensions(url) { const { imgHeight, imgWidth, imgUrl } = this.state; const urlValidity = AnnotationCreation.checkURL(url) ? 1 : 2; try { const dimensions = await AnnotationCreation.loadImg(url); this.setState({ imgHeight: { ...imgHeight, srcValue: dimensions.height || '', value: dimensions.height || '', }, imgUrl: { ...imgUrl, validity: 1, value: url, }, imgWidth: { ...imgWidth, srcValue: dimensions.width || '', value: dimensions.width || '', }, }); } catch (e) { this.setState({ imgUrl: { ...imgUrl, validity: urlValidity, value: url, }, }); } } /** */ setImgWidth(value) { const { imgWidth } = this.state; this.setState({ imgWidth: { ...imgWidth, value, }, }); } /** */ setImgUrl(value) { const { imgUrl } = this.state; this.setState({ imgUrl: { ...imgUrl, value, }, }); } /** */ setImgHeight(value) { const { imgHeight } = this.state; this.setState({ imgHeight: { ...imgHeight, value, }, }); } /** */ isConstrained(event) { // adjust other dimension in proportion to inputted dimension const { imgConstrain, imgWidth, imgHeight } = this.state; const ratio = imgWidth.srcValue / imgHeight.srcValue; if (imgConstrain) { if (event.target.id === 'width' && imgWidth.srcValue !== '') { // set height to be the same as width if width is less than 0 const height = imgWidth.value > 0 ? imgWidth.value * (1 / ratio) : imgWidth.value; this.setState({ imgHeight: { ...imgHeight, validity: 1, value: height, }, }); } else if (event.target.id === 'height' && imgHeight.srcValue !== '') { // set width to be the same as height if height is less than 0 const width = imgHeight.value > 0 ? imgHeight.value * ratio : imgHeight.value; this.setState({ imgWidth: { ...imgWidth, validity: 1, value: width, }, }); } } } /** 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) }); } /** update annotation start time */ updateTstart(value) { this.setState({ tstart: value }); } /** update annotation end time */ updateTend(value) { this.setState({ tend: value }); } /** 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)); } } /** */ 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, imgWidth, imgHeight, imgUrl, tags, xywh, svg, imgConstrain,tstart, tend, textEditorStateBustingKey, } = this.state; const annoBody = { value: textBody }; const t = (tstart && tend) ? `${tstart},${tend}` : null; if (imgWidth.validity === 1 && imgHeight.validity === 1 && imgUrl.validity === 1) { imgBody = { constrain: imgConstrain, h: imgHeight.value, url: imgUrl.value, w: imgWidth.value, }; } else { imgBody = image; } canvases.forEach((canvas) => { const storageAdapter = config.annotation.adapter(canvas.id); const anno = new WebAnnotation({ body: (!annoBody.length && t) ? `${secondsToHMS(tstart)} -> ${secondsToHMS(tend)}` : annoBody, canvasId: canvas.id, fragsel: { t, xywh }, id: (annotation && annotation.id) || `${uuid()}`, image: imgBody, 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({ textBody: '', svg: null, tend: null, 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, openAddImgDialog, popoverAnchorEl, strokeColor, popoverLineWeightAnchorEl, lineWeightPopoverOpen, strokeWidth, closedMode, textBody, imgUrl, imgWidth, imgHeight, imgConstrain, svg, tstart, tend, textEditorStateBustingKey, } = 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 start 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 }}> <ToggleButton value="image-icon" aria-label="insert an image" onClick={() => this.handleImgDialogChange(true)}> <InsertPhotoIcon /> </ToggleButton> </Grid> <Dialog open={openAddImgDialog} fullWidth onClose={() => this.handleImgDialogChange(false)} aria-labelledby="form-dialog-title"> <DialogTitle id="form-dialog-title" disableTypography> <Typography variant="h2">Insert image</Typography> </DialogTitle> <DialogContent> <DialogTitle id="form-dialog-subtitle-1" style={{ paddingLeft: 0 }} disableTypography> <Typography variant="h5">Image source</Typography> </DialogTitle> <TextField value={imgUrl.value} onChange={(e) => this.setImgUrl(e.target.value)} onBlur={(e) => this.getImgDimensions(e.target.value)} error={imgUrl.validity === 2} helperText={imgUrl.validity === 2 ? 'Invalid URL' : ''} margin="dense" id="source" label="Image URL" type="url" fullWidth /> </DialogContent> <DialogContent> <DialogTitle id="form-dialog-subtitle-2" style={{ paddingLeft: 0 }} disableTypography> <Typography variant="h5">Image dimensions</Typography> </DialogTitle> <TextField value={imgWidth.value} style={{ marginRight: 10, width: 100 }} onChange={(e) => this.setImgWidth(e.target.value)} onBlur={(e) => this.isConstrained(e)} error={imgWidth.validity === 2} helperText={imgWidth.validity === 2 ? 'Invalid width' : ''} margin="dense" id="width" label="Width" type="number" variant="outlined" /> <TextField value={imgHeight.value} style={{ marginLeft: 10, width: 100 }} onChange={(e) => this.setImgHeight(e.target.value)} onBlur={(e) => this.isConstrained(e)} error={imgHeight.validity === 2} helperText={imgHeight.validity === 2 ? 'Invalid height' : ''} margin="dense" id="height" label="Height" type="number" variant="outlined" /> <FormControlLabel control={( <Checkbox checked={imgConstrain} onChange={(e) => this.handleConstrainCheck(e)} inputProps={{ 'aria-label': 'primary checkbox' }} style={{ marginLeft: 30 }} /> )} label="Constrain proportions" /> </DialogContent> <DialogActions> <Button onClick={() => this.handleImgDialogChange(false)}>Cancel</Button> <Button variant="contained" onClick={this.handleImgDialogSubmit} color="primary">Add</Button> </DialogActions> </Dialog> <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);