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
No related branches found
No related tags found
1 merge request!5Create image annotation creation dialog
......@@ -2,7 +2,7 @@
"env": {
"jest/globals": true
},
"extends": ["airbnb","react-app"],
"extends": ["airbnb"],
"globals": {
"page": true,
"document": true
......
......@@ -3,7 +3,9 @@ import WebAnnotation from '../src/WebAnnotation';
/** */
function createSubject(args = {}) {
return new WebAnnotation({
body: 'body',
body: {
value: 'body',
},
canvasId: 'canvasId',
fragsel: { t: '5,10', xywh: 'xywh' },
id: 'id',
......@@ -17,11 +19,16 @@ describe('WebAnnotation', () => {
let subject = createSubject();
describe('constructor', () => {
it('sets instance accessors', () => {
['body', 'canvasId', 'id', 'svg'].forEach((prop) => {
['canvasId', 'id', 'svg'].forEach((prop) => {
expect(subject[prop]).toBe(prop);
});
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', () => {
it('with svg and xywh', () => {
......@@ -109,14 +116,14 @@ describe('WebAnnotation', () => {
]);
});
it('with text only', () => {
subject = createSubject({ tags: null });
subject = createSubject({ image: null, tags: null });
expect(subject.createBody()).toEqual({
type: 'TextualBody',
value: 'body',
});
});
it('with tags only', () => {
subject = createSubject({ body: null });
subject = createSubject({ body: null, image: null });
expect(subject.createBody()).toEqual({
purpose: 'tagging',
type: 'TextualBody',
......
Source diff could not be displayed: it is too large. Options to address this: view the blob.
......@@ -55,25 +55,53 @@ function geomFromAnnoTarget(annotarget) {
/** */
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') {
if (body.purpose === 'tagging' && body.type === 'TextualBody') {
annoState.tags.push(body.value);
} else {
annoState.annoBody = 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 {
annoState.annoBody = props.annotation.body.value;
} 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
......@@ -115,19 +143,42 @@ class AnnotationCreation extends Component {
this.state = {
...toolState,
...timeState,
annoBody: '',
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.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);
......@@ -143,6 +194,78 @@ class AnnotationCreation extends Component {
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,
});
}
/** */
......@@ -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 */
setTstartNow() {
// eslint-disable-next-line react/destructuring-assignment
......@@ -243,20 +467,37 @@ class AnnotationCreation extends Component {
annotation, canvases, receiveAnnotation, config,
} = this.props;
const {
annoBody, tags, xywh, svg, tstart, tend, textEditorStateBustingKey,
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);
......@@ -269,7 +510,7 @@ class AnnotationCreation extends Component {
});
this.setState({
annoBody: '',
textBody: '',
svg: null,
tend: null,
textEditorStateBustingKey: textEditorStateBustingKey + 1,
......@@ -293,8 +534,8 @@ class AnnotationCreation extends Component {
}
/** */
updateBody(annoBody) {
this.setState({ annoBody });
updateTextBody(textBody) {
this.setState({ textBody });
}
/** */
......@@ -311,9 +552,9 @@ class AnnotationCreation extends Component {
} = this.props;
const {
activeTool, colorPopoverOpen, currentColorType, fillColor, popoverAnchorEl, strokeColor,
popoverLineWeightAnchorEl, lineWeightPopoverOpen, strokeWidth, closedMode, annoBody, svg,
tstart, tend, textEditorStateBustingKey,
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';
......@@ -326,6 +567,7 @@ class AnnotationCreation extends Component {
>
<AnnotationDrawing
activeTool={activeTool}
annotation={annotation}
fillColor={fillColor}
strokeColor={strokeColor}
strokeWidth={strokeWidth}
......@@ -440,7 +682,6 @@ class AnnotationCreation extends Component {
)
: null
}
</Grid>
</Grid>
<Grid container>
......@@ -481,14 +722,92 @@ class AnnotationCreation extends Component {
)}
<Grid item xs={12}>
<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>
</Grid>
<Grid item xs={12}>
<TextEditor
key={textEditorStateBustingKey}
annoHtml={annoBody}
updateAnnotationBody={this.updateBody}
annoHtml={textBody}
updateAnnotationBody={this.updateTextBody}
/>
</Grid>
</Grid>
......
......@@ -2,7 +2,7 @@
export default class WebAnnotation {
/** */
constructor({
canvasId, id, fragsel, body, tags, svg, manifestId,
canvasId, id, fragsel, image, body, tags, svg, manifestId,
}) {
this.id = id;
this.canvasId = canvasId;
......@@ -10,6 +10,7 @@ export default class WebAnnotation {
this.body = body;
this.tags = tags;
this.svg = svg;
this.image = image;
this.manifestId = manifestId;
}
......@@ -18,7 +19,7 @@ export default class WebAnnotation {
return {
body: this.createBody(),
id: this.id,
motivation: 'commenting',
motivation: 'supplementing',
target: this.target(),
type: 'Annotation',
};
......@@ -27,12 +28,24 @@ export default class WebAnnotation {
/** */
createBody() {
let bodies = [];
if (this.body) {
bodies.push({
if (this.body && this.body.value !== '') {
const textBody = {
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) {
bodies = bodies.concat(this.tags.map((tag) => ({
purpose: 'tagging',
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment