labo-components
Version:
282 lines (248 loc) • 8.59 kB
JSX
import React from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import IDUtil from "../../../util/IDUtil";
import SegmentLayerHeader from "./SegmentLayerHeader";
import Segment from "./Segment";
import SelectionTemporalForm from "./SelectionTemporalForm";
import SelectionSpatialForm from "./SelectionSpatialForm";
import Strings from "../_Strings";
import { AnnotationEvents } from "../AnnotationClient";
import {
SELECTION_TEMPORAL,
SELECTION_SPATIAL,
ANNOTATION_COLUMN_SEGMENT_ID,
} from "../../../util/AnnotationConstants";
export default class SegmentAnnotations extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
selectionType: this.getSelectionType(this.props.mediaObject),
};
this.lastActiveAnnotationId = "";
}
componentDidMount() {
this.props.annotationClient.events.bind(
AnnotationEvents.ON_SET_ANNOTATION,
this.onUpdate
);
}
componentWillUnmount() {
this.props.annotationClient.events.unbind(
AnnotationEvents.ON_SET_ANNOTATION,
this.onUpdate
);
}
onUpdate = (data) => {
if (data && data.annotation && data.annotation.id) {
// skip if already active
if (this.lastActiveAnnotationId == data.annotation.id) {
return;
}
// scroll segment into view
const elem = document.getElementById(
ANNOTATION_COLUMN_SEGMENT_ID + data.annotation.id
);
if (elem) {
elem.scrollIntoView();
this.lastActiveAnnotationId = data.annotation.id;
// Prevent the column itself from scrolling
const column = (document.getElementsByClassName(
"bg__annotation-column"
)[0].scrollTop = 0);
}
}
};
getSelectionType(mediaObject) {
const mimeType = mediaObject.mimeType;
switch (true) {
case mimeType.startsWith("video"):
case mimeType.startsWith("audio"):
return SELECTION_TEMPORAL;
case mimeType.startsWith("image"):
return SELECTION_SPATIAL;
default:
return "";
}
}
toggle = () => {
this.props.toggle(this.props.layerId);
};
// Create a new segment, set it to 0:00
async createSegment(selection) {
// set activeAnnotation to null, so a new root annotation is created & activated (in saveSelection)
this.props.annotationClient.activeAnnotation = null;
// Create and activate a new segment for current layerId
await this.props.annotationClient.saveSelection(
selection,
true,
true,
this.props.layerId
);
}
// Update segment with given selection
// If the selection is null, delete the segment
async updateSegment(segment, selection) {
// When the selection is null: delete the segment
if (selection == null) {
this.props.annotationClient.delete(segment);
return;
}
// make the segment the active annotation, so it will be updated
this.props.annotationClient.activeAnnotation = segment;
// set the active selection
this.props.annotationClient.activeSelection = selection;
// Update the segment
await this.props.annotationClient.saveSelection(selection);
}
onUpdateSegment = (segment, selection) => {
this.updateSegment(segment, selection);
};
onAddSegment = (selection) => {
this.createSegment(selection);
};
onChangeTitle = (title) => {
this.props.annotationClient.segmentLayers.renameLayer(
this.props.layerId,
title
);
};
onDeleteLayer = () => {
this.props.annotationClient.segmentLayers.deleteLayerWithCheck(
this.props.layerId
);
};
createTemporalSelection = (start, end) => ({
type: "temporal",
start,
end,
});
createSpatialSelection = (x, y, w, h) => ({
type: "spatial",
rect: { x, y, w, h },
});
createSelection(selectionType) {
switch (selectionType) {
case SELECTION_TEMPORAL:
return this.createTemporalSelection(0, 0);
case SELECTION_SPATIAL:
return this.createSpatialSelection(0, 0, 0, 0);
default:
console.error(
"Could not create selection for type",
selectionType
);
}
}
renderForm(selection) {
switch (selection.type) {
case SELECTION_TEMPORAL:
return (
<SelectionTemporalForm
selection={selection}
onUpdate={this.onAddSegment}
/>
);
case SELECTION_SPATIAL:
return (
<SelectionSpatialForm
selection={selection}
onUpdate={this.onAddSegment}
/>
);
default:
console.error("Could not render form for type ", selectionType);
}
}
sortSegments = (segments, selectionType) => {
switch (selectionType) {
case SELECTION_TEMPORAL:
// start -> end
return segments.sort((a, b) => {
a = a.target.selector.refinedBy;
b = b.target.selector.refinedBy;
// fallback for erronous values
if (a === null) {
return b;
}
if (b === null) {
return a;
}
if (a.start == b.start) {
return a.end - b.end;
}
return a.start - b.start;
});
case SELECTION_SPATIAL:
// top -> down
return segments.sort((a, b) => {
a = a.target.selector.refinedBy.rect;
b = b.target.selector.refinedBy.rect;
if (!a || !b) {
return 0;
}
if (a.y == b.y) {
return a.x - b.x;
}
return a.y - b.y;
});
default:
return segments;
}
};
render() {
// count
const count = this.props.segments ? this.props.segments.length : 0;
// header
const header = (
<SegmentLayerHeader
active={this.props.active}
onToggle={this.toggle}
title={this.props.title}
count={count}
onChangeTitle={this.onChangeTitle}
onDeleteLayer={this.onDeleteLayer}
/>
);
// Form
const form = this.props.active
? this.renderForm(this.createSelection(this.state.selectionType))
: null;
// All segments
const segments = this.props.active
? this.sortSegments(
this.props.segments,
this.state.selectionType
).map((segment) => (
<Segment
key={segment.id}
segment={segment}
updateSegment={this.onUpdateSegment}
{...this.props}
/>
))
: null;
return this.state.selectionType === "" ? null : (
<div
className={classNames(
IDUtil.cssClassName("segment-annotations")
)}
>
{header}
{form}
{segments}
</div>
);
}
}
SegmentAnnotations.propTypes = {
annotationClient: PropTypes.object.isRequired,
title: PropTypes.string.isRequired,
target: PropTypes.string.isRequired,
layerId: PropTypes.number.isRequired,
toggle: PropTypes.func.isRequired,
active: PropTypes.bool.isRequired,
activeTypes: PropTypes.arrayOf(PropTypes.string).isRequired,
segments: PropTypes.arrayOf(PropTypes.object),
mediaObject: PropTypes.object.isRequired,
};