Skip to content
Snippets Groups Projects
Verified Commit 3b4d3851 authored by Loïs Poujade's avatar Loïs Poujade
Browse files

Merge remote-tracking branch 'Add-img-annotation-insert-edit' into 5-final-images

parents e79e44b3 7338be3a
Branches
No related tags found
No related merge requests found
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
"env": { "env": {
"jest/globals": true "jest/globals": true
}, },
"extends": ["airbnb","react-app"], "extends": ["airbnb"],
"globals": { "globals": {
"page": true, "page": true,
"document": true "document": true
......
...@@ -3,7 +3,9 @@ import WebAnnotation from '../src/WebAnnotation'; ...@@ -3,7 +3,9 @@ import WebAnnotation from '../src/WebAnnotation';
/** */ /** */
function createSubject(args = {}) { function createSubject(args = {}) {
return new WebAnnotation({ return new WebAnnotation({
body: 'body', body: {
value: 'body',
},
canvasId: 'canvasId', canvasId: 'canvasId',
fragsel: { t: '5,10', xywh: 'xywh' }, fragsel: { t: '5,10', xywh: 'xywh' },
id: 'id', id: 'id',
...@@ -17,11 +19,16 @@ describe('WebAnnotation', () => { ...@@ -17,11 +19,16 @@ describe('WebAnnotation', () => {
let subject = createSubject(); let subject = createSubject();
describe('constructor', () => { describe('constructor', () => {
it('sets instance accessors', () => { it('sets instance accessors', () => {
['body', 'canvasId', 'id', 'svg'].forEach((prop) => { ['canvasId', 'id', 'svg'].forEach((prop) => {
expect(subject[prop]).toBe(prop); expect(subject[prop]).toBe(prop);
}); });
expect(subject.fragsel).toStrictEqual({ t: '5,10', xywh: 'xywh' }); expect(subject.fragsel).toStrictEqual({ t: '5,10', xywh: 'xywh' });
}); });
it('sets instance accessors for body', () => {
['body'].forEach((prop) => {
expect(subject[prop].value).toBe(prop);
});
});
}); });
describe('target', () => { describe('target', () => {
it('with svg and xywh', () => { it('with svg and xywh', () => {
...@@ -109,14 +116,14 @@ describe('WebAnnotation', () => { ...@@ -109,14 +116,14 @@ describe('WebAnnotation', () => {
]); ]);
}); });
it('with text only', () => { it('with text only', () => {
subject = createSubject({ tags: null }); subject = createSubject({ image: null, tags: null });
expect(subject.createBody()).toEqual({ expect(subject.createBody()).toEqual({
type: 'TextualBody', type: 'TextualBody',
value: 'body', value: 'body',
}); });
}); });
it('with tags only', () => { it('with tags only', () => {
subject = createSubject({ body: null }); subject = createSubject({ body: null, image: null });
expect(subject.createBody()).toEqual({ expect(subject.createBody()).toEqual({
purpose: 'tagging', purpose: 'tagging',
type: 'TextualBody', type: 'TextualBody',
......
This diff is collapsed.
...@@ -55,25 +55,53 @@ function geomFromAnnoTarget(annotarget) { ...@@ -55,25 +55,53 @@ function geomFromAnnoTarget(annotarget) {
/** */ /** */
class AnnotationCreation extends Component { 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) { constructor(props) {
super(props); super(props);
const annoState = {}; const annoState = {};
annoState.image = false;
console.log('edition/creation annotation: ', props.annotation);
if (props.annotation) { if (props.annotation) {
// //
// annotation body // annotation body
if (Array.isArray(props.annotation.body)) { if (Array.isArray(props.annotation.body)) {
annoState.tags = []; annoState.tags = [];
props.annotation.body.forEach((body) => { props.annotation.body.forEach((body) => {
if (body.purpose === 'tagging') { if (body.purpose === 'tagging' && body.type === 'TextualBody') {
annoState.tags.push(body.value); annoState.tags.push(body.value);
} else { } else if (body.type === 'TextualBody') {
annoState.annoBody = body.value; annoState.textBody = body.value;
} else if (body.type === 'Image') {
annoState.textBody = body.value;
annoState.image = body.image;
} }
}); });
} else { } else if (props.annotation.body.type === 'TextualBody') {
annoState.annoBody = props.annotation.body.value; 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 // drawing position
...@@ -115,19 +143,42 @@ class AnnotationCreation extends Component { ...@@ -115,19 +143,42 @@ class AnnotationCreation extends Component {
this.state = { this.state = {
...toolState, ...toolState,
...timeState, ...timeState,
annoBody: '', textBody: '',
colorPopoverOpen: false, colorPopoverOpen: false,
lineWeightPopoverOpen: false, lineWeightPopoverOpen: false,
openAddImgDialog: false,
popoverAnchorEl: null, popoverAnchorEl: null,
popoverLineWeightAnchorEl: null, popoverLineWeightAnchorEl: null,
svg: null, svg: null,
textEditorStateBustingKey: 0, textEditorStateBustingKey: 0,
imgConstrain: false,
imgHeight: {
lastSubmittedValue: '',
srcValue: '',
validity: 0,
value: '',
},
imgUrl: {
lastSubmittedValue: '',
validity: 0,
value: '',
},
imgWidth: {
lastSubmittedValue: '',
srcValue: '',
validity: 0,
value: '',
},
xywh: null, xywh: null,
...annoState, ...annoState,
}; };
this.submitForm = this.submitForm.bind(this); this.submitForm = this.submitForm.bind(this);
this.updateBody = this.updateBody.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.updateTstart = this.updateTstart.bind(this);
this.updateTend = this.updateTend.bind(this); this.updateTend = this.updateTend.bind(this);
this.setTstartNow = this.setTstartNow.bind(this); this.setTstartNow = this.setTstartNow.bind(this);
...@@ -143,6 +194,78 @@ class AnnotationCreation extends Component { ...@@ -143,6 +194,78 @@ class AnnotationCreation extends Component {
this.handleCloseLineWeight = this.handleCloseLineWeight.bind(this); this.handleCloseLineWeight = this.handleCloseLineWeight.bind(this);
this.closeChooseColor = this.closeChooseColor.bind(this); this.closeChooseColor = this.closeChooseColor.bind(this);
this.updateStrokeColor = this.updateStrokeColor.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,
});
} }
/** */ /** */
...@@ -162,6 +285,107 @@ class AnnotationCreation extends Component { ...@@ -162,6 +285,107 @@ class AnnotationCreation extends Component {
}); });
} }
/** */
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 */ /** set annotation start time to current time */
setTstartNow() { setTstartNow() {
// eslint-disable-next-line react/destructuring-assignment // eslint-disable-next-line react/destructuring-assignment
...@@ -243,20 +467,37 @@ class AnnotationCreation extends Component { ...@@ -243,20 +467,37 @@ class AnnotationCreation extends Component {
annotation, canvases, receiveAnnotation, config, annotation, canvases, receiveAnnotation, config,
} = this.props; } = this.props;
const { const {
annoBody, tags, xywh, svg, tstart, tend, textEditorStateBustingKey, textBody, image, imgWidth, imgHeight, imgUrl, tags, xywh, svg,
imgConstrain,tstart, tend, textEditorStateBustingKey,
} = this.state; } = this.state;
const annoBody = { value: textBody };
const t = (tstart && tend) ? `${tstart},${tend}` : null; 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) => { canvases.forEach((canvas) => {
const storageAdapter = config.annotation.adapter(canvas.id); const storageAdapter = config.annotation.adapter(canvas.id);
const anno = new WebAnnotation({ const anno = new WebAnnotation({
body: (!annoBody.length && t) ? `${secondsToHMS(tstart)} -> ${secondsToHMS(tend)}` : annoBody, body: (!annoBody.length && t) ? `${secondsToHMS(tstart)} -> ${secondsToHMS(tend)}` : annoBody,
canvasId: canvas.id, canvasId: canvas.id,
fragsel: { t, xywh }, fragsel: { t, xywh },
id: (annotation && annotation.id) || `${uuid()}`, id: (annotation && annotation.id) || `${uuid()}`,
image: imgBody,
manifestId: canvas.options.resource.id, manifestId: canvas.options.resource.id,
svg, svg,
tags, tags,
}).toJson(); }).toJson();
if (annotation) { if (annotation) {
storageAdapter.update(anno).then((annoPage) => { storageAdapter.update(anno).then((annoPage) => {
receiveAnnotation(canvas.id, storageAdapter.annotationPageId, annoPage); receiveAnnotation(canvas.id, storageAdapter.annotationPageId, annoPage);
...@@ -269,7 +510,7 @@ class AnnotationCreation extends Component { ...@@ -269,7 +510,7 @@ class AnnotationCreation extends Component {
}); });
this.setState({ this.setState({
annoBody: '', textBody: '',
svg: null, svg: null,
tend: null, tend: null,
textEditorStateBustingKey: textEditorStateBustingKey + 1, textEditorStateBustingKey: textEditorStateBustingKey + 1,
...@@ -293,8 +534,8 @@ class AnnotationCreation extends Component { ...@@ -293,8 +534,8 @@ class AnnotationCreation extends Component {
} }
/** */ /** */
updateBody(annoBody) { updateTextBody(textBody) {
this.setState({ annoBody }); this.setState({ textBody });
} }
/** */ /** */
...@@ -311,9 +552,9 @@ class AnnotationCreation extends Component { ...@@ -311,9 +552,9 @@ class AnnotationCreation extends Component {
} = this.props; } = this.props;
const { const {
activeTool, colorPopoverOpen, currentColorType, fillColor, popoverAnchorEl, strokeColor, activeTool, colorPopoverOpen, currentColorType, fillColor, openAddImgDialog, popoverAnchorEl,
popoverLineWeightAnchorEl, lineWeightPopoverOpen, strokeWidth, closedMode, annoBody, svg, strokeColor, popoverLineWeightAnchorEl, lineWeightPopoverOpen, strokeWidth, closedMode,
tstart, tend, textEditorStateBustingKey, textBody, imgUrl, imgWidth, imgHeight, imgConstrain, svg, tstart, tend, textEditorStateBustingKey,
} = this.state; } = this.state;
const mediaIsVideo = typeof VideosReferences.get(windowId) !== 'undefined'; const mediaIsVideo = typeof VideosReferences.get(windowId) !== 'undefined';
...@@ -326,6 +567,7 @@ class AnnotationCreation extends Component { ...@@ -326,6 +567,7 @@ class AnnotationCreation extends Component {
> >
<AnnotationDrawing <AnnotationDrawing
activeTool={activeTool} activeTool={activeTool}
annotation={annotation}
fillColor={fillColor} fillColor={fillColor}
strokeColor={strokeColor} strokeColor={strokeColor}
strokeWidth={strokeWidth} strokeWidth={strokeWidth}
...@@ -440,7 +682,6 @@ class AnnotationCreation extends Component { ...@@ -440,7 +682,6 @@ class AnnotationCreation extends Component {
) )
: null : null
} }
</Grid> </Grid>
</Grid> </Grid>
<Grid container> <Grid container>
...@@ -481,14 +722,92 @@ class AnnotationCreation extends Component { ...@@ -481,14 +722,92 @@ class AnnotationCreation extends Component {
)} )}
<Grid item xs={12}> <Grid item xs={12}>
<Typography variant="overline"> <Typography variant="overline">
Content 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> </Typography>
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
<TextEditor <TextEditor
key={textEditorStateBustingKey} key={textEditorStateBustingKey}
annoHtml={annoBody} annoHtml={textBody}
updateAnnotationBody={this.updateBody} updateAnnotationBody={this.updateTextBody}
/> />
</Grid> </Grid>
</Grid> </Grid>
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
export default class WebAnnotation { export default class WebAnnotation {
/** */ /** */
constructor({ constructor({
canvasId, id, fragsel, body, tags, svg, manifestId, canvasId, id, fragsel, image, body, tags, svg, manifestId,
}) { }) {
this.id = id; this.id = id;
this.canvasId = canvasId; this.canvasId = canvasId;
...@@ -10,6 +10,7 @@ export default class WebAnnotation { ...@@ -10,6 +10,7 @@ export default class WebAnnotation {
this.body = body; this.body = body;
this.tags = tags; this.tags = tags;
this.svg = svg; this.svg = svg;
this.image = image;
this.manifestId = manifestId; this.manifestId = manifestId;
} }
...@@ -18,7 +19,7 @@ export default class WebAnnotation { ...@@ -18,7 +19,7 @@ export default class WebAnnotation {
return { return {
body: this.createBody(), body: this.createBody(),
id: this.id, id: this.id,
motivation: 'commenting', motivation: 'supplementing',
target: this.target(), target: this.target(),
type: 'Annotation', type: 'Annotation',
}; };
...@@ -27,12 +28,24 @@ export default class WebAnnotation { ...@@ -27,12 +28,24 @@ export default class WebAnnotation {
/** */ /** */
createBody() { createBody() {
let bodies = []; let bodies = [];
if (this.body) {
bodies.push({ if (this.body && this.body.value !== '') {
const textBody = {
type: 'TextualBody', type: 'TextualBody',
value: this.body, value: this.body.value,
}); };
bodies.push(textBody);
} }
if (this.image) {
const imgBody = {
format: 'image/jpg',
id: this.image.url,
type: 'Image',
};
bodies.push(imgBody);
}
if (this.tags) { if (this.tags) {
bodies = bodies.concat(this.tags.map((tag) => ({ bodies = bodies.concat(this.tags.map((tag) => ({
purpose: 'tagging', purpose: 'tagging',
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment