/** An AnnotationPane holds annotations. */
class AnnotationPane {
/**
* Create an annotation pane.
* @param {Object} options - Configuration options.
* @author Lawrence Fyfe
*/
constructor(options) {
this.paneType = "annotation";
this.options = options;
this.annotationColor = options?.color ?? "#800000";
this.panelWidth = options?.panelWidth ?? "100%";
this.showPanelTitle = options?.showPanelTitle ?? true;
this.parentElement;
this.width;
this.height;
this.timeScale;
this.initialized = false;
this.typeMode = AnnotationType.NONE;
this.allowBoundaries = true;
this.allowRegions = true;
this.allowComments = true;
this.allowMarkers = true;
this.allowNoteGroups = true;
this.allowSaving = true;
this.boundaryArray = [];
this.commentArray = [];
this.markerArray = [];
this.noteGroupArray = [];
this.regionArray = [];
this.showBoundaries = true;
this.showComments = true;
this.showMarkers = true;
this.showNoteGroups = true;
this.showRegions = true;
this.annotationPaneGroup;
this.annotationsGroup;
this.boundaryAnnotationsGroup;
this.commentAnnotationsGroup;
this.noteGroupAnnotationsGroup;
this.regionAnnotationsGroup;
this.notes;
this.dragHandler;
this.regionSelectRect;
this.groupSelectRect;
this.onCreate;
this.onSelect;
this.onUpdate;
this.onUpdateLabel;
this.onDelete;
this.onSave;
this.panelElement;
this.panelInitialized = false;
this.panelOn = false;
this.typeValues = [AnnotationType.BOUNDARY, AnnotationType.COMMENT, AnnotationType.MARKER, AnnotationType.NOTE_GROUP, AnnotationType.REGION];
this.boundaryStrengths = [1, 2, 3, 4];
this.annotationPanelDiv;
this.annotationControlsDiv;
this.annotationTypeDiv;
this.annotationSaveButton;
this.saveButtonActivated = false;
}
/**
* Set the specified type mode for this pane where types are defined in AnnotationType. By setting the type mode, this annotation pane
* makes annotations of that type active and deactivates all other types. Activating a type of annotation means that annotations of that
* type can be created, updated, or deleted.
* @param {number} type - A type defined in AnnotationType.
*/
setTypeMode(type) {
this.typeMode = type;
switch (this.typeMode) {
case AnnotationType.BOUNDARY:
this.#activateAnnotationArray(this.boundaryArray, true);
this.#activateAnnotationArray(this.commentArray, false);
this.#activateAnnotationArray(this.markerArray, false);
this.#activateAnnotationArray(this.noteGroupArray, false);
this.#activateAnnotationArray(this.regionArray, false);
break;
case AnnotationType.COMMENT:
this.#activateAnnotationArray(this.boundaryArray, false);
this.#activateAnnotationArray(this.commentArray, true);
this.#activateAnnotationArray(this.markerArray, false);
this.#activateAnnotationArray(this.noteGroupArray, false);
this.#activateAnnotationArray(this.regionArray, false);
break;
case AnnotationType.MARKER:
this.#activateAnnotationArray(this.boundaryArray, false);
this.#activateAnnotationArray(this.commentArray, false);
this.#activateAnnotationArray(this.markerArray, true);
this.#activateAnnotationArray(this.noteGroupArray, false);
this.#activateAnnotationArray(this.regionArray, false);
break;
case AnnotationType.NOTE_GROUP:
this.#activateAnnotationArray(this.boundaryArray, false);
this.#activateAnnotationArray(this.commentArray, false);
this.#activateAnnotationArray(this.markerArray, false);
this.#activateAnnotationArray(this.noteGroupArray, true);
this.#activateAnnotationArray(this.regionArray, false);
break;
case AnnotationType.REGION:
this.#activateAnnotationArray(this.boundaryArray, false);
this.#activateAnnotationArray(this.commentArray, false);
this.#activateAnnotationArray(this.markerArray, false);
this.#activateAnnotationArray(this.noteGroupArray, false);
this.#activateAnnotationArray(this.regionArray, true);
break;
case AnnotationType.NONE:
this.#activateAnnotationArray(this.boundaryArray, false);
this.#activateAnnotationArray(this.commentArray, false);
this.#activateAnnotationArray(this.markerArray, false);
this.#activateAnnotationArray(this.noteGroupArray, false);
this.#activateAnnotationArray(this.regionArray, false);
break;
}
}
/**
* Get the current type mode where types are defined in AnnotationType.
* @return {AnnotationType} A constant representing the type.
*/
getTypeMode() {
return this.typeMode;
}
/**
* Generate a boundary annotation with the specified parameters and return it. If the type mode for this annotation pane
* is set to BOUNDARY, the newly-generated Boundary will be active.
* @param {number} time - The time.
* @param {number} strength - The strength.
* @param {string} label - A label.
* @param {Object} properties - Properties.
* @param {boolean} firstCreated - Whether this boundary was created for the first time.
* @return {Boundary} A newly-generated Boundary.
*/
generateBoundary(time, strength, label, properties, firstCreated) {
var active = (this.typeMode === AnnotationType.BOUNDARY) ? true : false;
return new Boundary(time, strength, label, this.options, properties, active, firstCreated);
}
/**
* Add a boundary annotation with the specified parameters. The boundary will have a status of UNCHANGED (see AnnotationStatus for other status values).
* This method is most useful for adding saved boundaries to an annotation pane.
* @param {number} time - The time.
* @param {number} strength - The strength.
* @param {string} label - A label.
* @param {Object} properties - Properties.
*/
addBoundary(time, strength, label, properties) {
this.boundaryArray.push(this.generateBoundary(time, strength, label, properties, false));
}
/**
* Create a new boundary annotation with a status of CREATED and run onCreate() if it is set. This method is used for interactively-created boundaries.
* @param {number} time - The time.
* @param {number} strength - The strength.
* @param {string} label - A label.
* @param {Object} properties - Properties.
*/
createBoundary(time, strength, label, properties) {
if (this.typeMode !== AnnotationType.BOUNDARY) {
this.typeMode = AnnotationType.BOUNDARY;
}
var boundary = this.generateBoundary(time, strength, label, properties, true);
this.boundaryArray.push(boundary);
if (this.onCreate !== undefined) {
this.onCreate(boundary);
}
}
/**
* Generate a comment annotation with the specified parameters and return it. If the type mode for this annotation pane
* is set to COMMENT, the newly-generated Comment will be active.
* @param {number} time - The time.
* @param {string} label - A label.
* @param {Object} properties - Properties.
* @param {boolean} firstCreated - Whether this comment was created for the first time.
* @return {Comment} A newly-generated Comment.
*/
generateComment(time, label, properties, firstCreated) {
var active = (this.typeMode === AnnotationType.COMMENT) ? true : false;
return new Comment(time, label, this.options, properties, active, firstCreated);
}
/**
* Add a comment annotation with the specified parameters. The comment will have a status of UNCHANGED (see AnnotationStatus for other status values).
* This method is most useful for adding saved comments to an annotation pane.
* @param {number} time - The time.
* @param {string} label - A label.
* @param {Object} properties - Properties.
*/
addComment(time, label, properties) {
this.commentArray.push(this.generateComment(time, label, properties, false));
}
/**
* Create a new comment annotation with a status of CREATED and run onCreate() if it is set. This method is used for interactively-created comments.
* @param {number} time - The time.
* @param {string} label - A label.
* @param {Object} properties - Properties.
*/
createComment(time, label, properties) {
if (this.typeMode !== AnnotationType.COMMENT) {
this.typeMode = AnnotationType.COMMENT;
}
var comment = this.generateComment(time, label, properties, true);
this.commentArray.push(comment);
if (this.onCreate !== undefined) {
this.onCreate(comment);
}
}
/**
* Generate a marker annotation with the specified parameters and return it. If the type mode for this annotation pane
* is set to MARKER, the newly-generated Marker will be active.
* @param {number} time - The time.
* @param {string} label - A label.
* @param {Object} properties - Properties.
* @param {boolean} firstCreated - Whether this comment was created for the first time.
* @return {Marker} A newly-generated Marker.
*/
generateMarker(time, label, properties, firstCreated) {
var active = (this.typeMode === AnnotationType.MARKER) ? true : false;
return new Marker(time, label, this.options, properties, active, firstCreated);
}
/**
* Add a marker annotation with the specified parameters. The marker will have a status of UNCHANGED (see AnnotationStatus for other status values).
* This method is most useful for adding saved markers to an annotation pane.
* @param {number} time - The time.
* @param {string} label - A label.
* @param {Object} properties - Properties.
*/
addMarker(time, label, properties) {
this.markerArray.push(this.generateMarker(time, label, properties, false));
}
/**
* Create a new marker annotation with a status of CREATED and run onCreate() if it is set. This method is used for interactively-created markers.
* @param {number} time - The time.
* @param {string} label - A label.
* @param {Object} properties - Properties.
*/
createMarker(time, label, properties) {
if (this.typeMode !== AnnotationType.MARKER) {
this.typeMode = AnnotationType.MARKER;
}
var marker = this.generateMarker(time, label, properties, true);
this.markerArray.push(marker);
if (this.onCreate !== undefined) {
this.onCreate(marker);
}
}
/**
* Generate a note group annotation with the specified parameters and return it. If the type mode for this annotation pane
* is set to NOTE_GROUP, the newly-generated NoteGroup will be active.
* @param {Array} noteArray - An array of notes.
* @param {string} label - A label.
* @param {Object} properties - Properties.
* @param {boolean} firstCreated - Whether this comment was created for the first time.
* @return {NoteGroup} A newly-generated NoteGroup.
*/
generateNoteGroup(noteArray, label, properties, firstCreated) {
var active = (this.typeMode === AnnotationType.NOTE_GROUP) ? true : false;
return new NoteGroup(noteArray, this.notes.noteScale, label, this.options, properties, active, firstCreated);
}
/**
* Add a note group annotation with the specified parameters. The note group will have a status of UNCHANGED (see AnnotationStatus for other status values).
* This method is most useful for adding saved markers to an annotation pane.
* @param {Array} noteArray - An array of notes.
* @param {string} label - A label.
* @param {Object} properties - Properties.
*/
addNoteGroup(noteArray, label, properties) {
if (this.notes !== undefined) {
this.noteGroupArray.push(this.generateNoteGroup(noteArray, label, properties, false));
}
}
/**
* Create a new note group annotation with a status of CREATED and run onCreate() if it is set. This method is used for interactively-created note groups.
* @param {Array} noteArray - An array of notes.
* @param {string} label - A label.
* @param {Object} properties - Properties.
*/
createNoteGroup(noteArray, label, properties) {
if (this.typeMode !== AnnotationType.NOTE_GROUP) {
this.typeMode = AnnotationType.NOTE_GROUP;
}
if (this.notes !== undefined) {
let noteGroup = this.generateNoteGroup(noteArray, label, properties, true);
this.noteGroupArray.push(noteGroup);
this.#deselectOthers(noteGroup);
if (this.onCreate !== undefined) {
this.onCreate(noteGroup);
}
}
}
/**
* Generate a region annotation with the specified parameters and return it. If the type mode for this annotation pane
* is set to REGION, the newly-generated Region will be active.
* @param {number} startTime - The start time.
* @param {number} endTime - The end time.
* @param {string} label - A label.
* @param {Object} properties - Properties.
* @param {boolean} firstCreated - Whether this comment was created for the first time.
* @return {Region} A newly-generated Region.
*/
generateRegion(startTime, endTime, label, properties, firstCreated) {
var active = (this.typeMode === AnnotationType.REGION) ? true : false;
return new Region(startTime, endTime, label, this.options, properties, active, firstCreated)
}
/**
* Add a region annotation with the specified parameters. The region will have a status of UNCHANGED (see AnnotationStatus for other status values).
* This method is most useful for adding saved markers to an annotation pane.
* @param {number} startTime - The start time.
* @param {number} endTime - The end time.
* @param {string} label - A label.
* @param {Object} properties - Properties.
*/
addRegion(startTime, endTime, label, properties) {
this.regionArray.push(this.generateRegion(startTime, endTime, label, properties, false));
}
/**
* Create a new region annotation with a status of CREATED and run onCreate() if it is set. This method is used for interactively-created regions.
* @param {number} startTime - The start time.
* @param {number} endTime - The end time.
* @param {string} label - A label.
* @param {Object} properties - Properties.
*/
createRegion(startTime, endTime, label, properties) {
if (this.typeMode !== AnnotationType.REGION) {
this.typeMode = AnnotationType.REGION;
}
var region = this.generateRegion(startTime, endTime, label, properties, true);
this.regionArray.push(region);
if (this.onCreate !== undefined) {
this.onCreate(region);
}
}
/**
* Add an array of annotations of the specified type to this pane.
* @param {AnnotationType} type - The type of annotation.
* @param {Array} annotations - An array of annotations.
*/
addAnnotations(type, annotations) {
switch (type) {
case AnnotationType.BOUNDARY:
this.boundaryArray = [...this.boundaryArray, ...annotations];
break;
case AnnotationType.COMMENT:
this.commentArray = [...this.commentArray, ...annotations];
break;
case AnnotationType.MARKER:
this.markerArray = [...this.markerArray, ...annotations];
break;
case AnnotationType.NOTE_GROUP:
this.noteGroupArray = [...this.noteGroupArray, ...annotations];
break;
case AnnotationType.REGION:
this.regionArray = [...this.regionArray, ...annotations];
break;
}
}
/**
* Activate all annotations in this pane's annotation array.
*/
#activateAnnotationArray(annotationArray, activate) {
if (annotationArray.length > 0) {
for (let i = 0; i < annotationArray.length; i++) {
annotationArray[i].active = activate;
}
}
}
/**
* Show or hide annotations of the specified type.
* @param {AnnotationType} type - The type of annotation.
* @param {boolean} show - True shows annotations and false hides them.
*/
showAnnotationType(type, show) {
switch (type) {
case AnnotationType.BOUNDARY:
this.showBoundaries = show;
if (show) {
this.drawBoundaries();
}
else {
this.eraseBoundaries();
}
break;
case AnnotationType.COMMENT:
this.showComments = show;
if (show) {
this.drawComments();
}
else {
this.eraseComments();
}
break;
case AnnotationType.MARKER:
this.showMarkers = show;
if (show) {
this.drawMarkers();
}
else {
this.eraseMarkers();
}
break;
case AnnotationType.NOTE_GROUP:
this.showNoteGroups = show;
if (show) {
this.drawNoteGroups();
}
else {
this.eraseNoteGroups();
}
break;
case AnnotationType.REGION:
this.showRegions = show;
if (show) {
this.drawRegions();
}
else {
this.eraseRegions();
}
break;
}
}
/**
* Show or hide annotations of the specified type and with the specified property that's equal to the specified value.
* @param {AnnotationType} type - The type of annotation.
* @param {string} propertyName - The name of the property.
* @param {string} propertyValue - The value of the property.
* @param {boolean} show - True shows annotations and false hides them.
*/
showAnnotationsByProperty(type, propertyName, propertyValue, show) {
switch (type) {
case AnnotationType.BOUNDARY:
for (let i = 0; i < this.boundaryArray.length; i++) {
if (this.boundaryArray[i].properties !== undefined && this.boundaryArray[i].properties[propertyName] === propertyValue) {
this.boundaryArray[i].show = show;
}
}
break;
case AnnotationType.COMMENT:
for (let i = 0; i < this.commentArray.length; i++) {
if (this.commentArray[i].properties !== undefined && this.commentArray[i].properties[propertyName] === propertyValue) {
this.commentArray[i].show = show;
}
}
break;
case AnnotationType.MARKER:
for (let i = 0; i < this.markerArray.length; i++) {
if (this.markerArray[i].properties !== undefined && this.markerArray[i].properties[propertyName] === propertyValue) {
this.markerArray[i].show = show;
}
}
break;
case AnnotationType.NOTE_GROUP:
for (let i = 0; i < this.noteGroupArray.length; i++) {
if (this.noteGroupArray[i].properties !== undefined && this.noteGroupArray[i].properties[propertyName] === propertyValue) {
this.noteGroupArray[i].show = show;
}
}
break;
case AnnotationType.REGION:
for (let i = 0; i < this.regionArray.length; i++) {
if (this.regionArray[i].properties !== undefined && this.regionArray[i].properties[propertyName] === propertyValue) {
this.regionArray[i].show = show;
}
}
break;
}
}
/**
* Set the show/hide value for all annotations.
* @param {boolean} show - True shows annotations and false hides them.
*/
showAllAnnotations(show) {
this.showBoundaries = show;
this.showComments = show;
this.showMarkers = show;
this.showNoteGroups = show;
this.showRegions = show;
}
/**
* Activate or deactivate all annotations.
* @param {boolean} activate - True activates annotations and false deactivates them.
*/
activateAnnotations(activate) {
this.#activateAnnotationArray(this.boundaryArray, activate);
this.#activateAnnotationArray(this.commentArray, activate);
this.#activateAnnotationArray(this.markerArray, activate);
this.#activateAnnotationArray(this.noteGroupArray, activate);
this.#activateAnnotationArray(this.regionArray, activate);
}
/**
* Deselect all annotations regardless of type.
*/
deselectAllAnnotations() {
if (this.boundaryArray.length > 0) {
for (let i = 0; i < this.boundaryArray.length; i++) {
this.boundaryArray[i].select(false);
}
}
if (this.commentArray.length > 0) {
for (let i = 0; i < this.commentArray.length; i++) {
this.commentArray[i].select(false);
}
}
if (this.markerArray.length > 0) {
for (let i = 0; i < this.markerArray.length; i++) {
this.markerArray[i].select(false);
}
}
if (this.regionArray.length > 0) {
for (let i = 0; i < this.regionArray.length; i++) {
this.regionArray[i].select(false);
}
}
if (this.noteGroupArray.length > 0) {
for (let i = 0; i < this.noteGroupArray.length; i++) {
this.noteGroupArray[i].select(false);
}
}
if (this.panelOn) {
this.drawAnnotationList();
}
}
/**
* Select or deselect all annotations of the specified type.
* @param {AnnotationType} type - The type of annotation.
* @param {boolean} selectAll - True selects all annotations and false deselects them.
*/
selectAllAnnotationsOfType(type, selectAll) {
switch (type) {
case AnnotationType.BOUNDARY:
if (this.boundaryArray.length > 0) {
for (let i = 0; i < this.boundaryArray.length; i++) {
this.boundaryArray[i].select(selectAll);
}
}
break;
case AnnotationType.COMMENT:
if (this.commentArray.length > 0) {
for (let i = 0; i < this.commentArray.length; i++) {
this.commentArray[i].select(selectAll);
}
}
break;
case AnnotationType.MARKER:
if (this.markerArray.length > 0) {
for (let i = 0; i < this.markerArray.length; i++) {
this.markerArray[i].select(selectAll);
}
}
break;
case AnnotationType.NOTE_GROUP:
if (this.noteGroupArray.length > 0) {
for (let i = 0; i < this.noteGroupArray.length; i++) {
this.noteGroupArray[i].select(selectAll);
}
}
break;
case AnnotationType.REGION:
if (this.regionArray.length > 0) {
for (let i = 0; i < this.regionArray.length; i++) {
this.regionArray[i].select(selectAll);
}
}
break;
}
}
/**
* Deselects all other annotations that are not the specified annotation.
* @param {annotation} annotation - The annotations that is not to be deselected.
*/
#deselectOthers(annotation) {
switch (this.typeMode) {
case AnnotationType.BOUNDARY:
if (this.boundaryArray.length > 0) {
for (let i = 0; i < this.boundaryArray.length; i++) {
if (this.boundaryArray[i] !== annotation) {
this.boundaryArray[i].select(false);
}
}
}
break;
case AnnotationType.COMMENT:
if (this.commentArray.length > 0) {
for (let i = 0; i < this.commentArray.length; i++) {
if (this.commentArray[i] !== annotation) {
this.commentArray[i].select(false);
}
}
}
break;
case AnnotationType.MARKER:
if (this.markerArray.length > 0) {
for (let i = 0; i < this.markerArray.length; i++) {
if (this.markerArray[i] !== annotation) {
this.markerArray[i].select(false);
}
}
}
break;
case AnnotationType.NOTE_GROUP:
if (this.noteGroupArray.length > 0) {
for (let i = 0; i < this.noteGroupArray.length; i++) {
if (this.noteGroupArray[i] !== annotation) {
this.noteGroupArray[i].select(false);
}
}
}
break;
case AnnotationType.REGION:
if (this.regionArray.length > 0) {
for (let i = 0; i < this.regionArray.length; i++) {
if (this.regionArray[i] !== annotation) {
this.regionArray[i].select(false);
}
}
}
break;
}
}
/**
* Select a note for creating a new note group or adding to an existing one. If a note group exists and is selected, then calling this method
* will add the note to that group. Otherwise the selected note will create a new note group.
* @param {Object} note - The annotations that is not to be deselected.
* @param {boolean} createGroup - The annotations that is not to be deselected.
*/
selectNote(note, createGroup) {
if (this.typeMode === AnnotationType.NOTE_GROUP && this.showNoteGroups) {
if (createGroup) {
let noteArray = [];
noteArray.push(note);
this.createNoteGroup(noteArray, "");
}
else {
let groupArray = this.getAnnotationsByType(AnnotationType.NOTE_GROUP);
if (groupArray.length > 0) {
for (let i = 0; i < groupArray.length; i++) {
if (!createGroup) {
if (groupArray[i].selected) {
groupArray[i].addNote(note);
if (this.onUpdate !== undefined) {
this.onUpdate(groupArray[i]);
}
}
}
}
}
}
this.drawPanel();
this.drawNoteGroups();
}
}
/**
* Select a note for creating a new note group or adding to an existing one. If a note group exists and is selected, then calling this method
* will add the note to that group. Otherwise the selected note will create a new note group.
* @param {number} t1 - The start of the time range.
* @param {number} t2 - The end of the time range.
* @param {number} y1 - The start of the value range.
* @param {number} y2 - The end of the value range.
* @param {boolean} createGroup - Whether to create a new note group.
* @param {boolean} deselectGroup - Whether notes in a group should be deleted from or added to a note group.
*/
selectNoteRange(t1, t2, y1, y2, createGroup, deselectGroup) {
if (this.notes !== undefined) {
let note1 = this.notes.noteScale.invert(y1);
let note2 = this.notes.noteScale.invert(y2);
let selectedNoteEvents = this.notes.getNotesWithinRanges(t1, t2, note1, note2);
if (selectedNoteEvents.length > 0) {
let noteArray = [];
for (let i = 0; i < selectedNoteEvents.length; i++) {
noteArray.push(selectedNoteEvents[i]);
}
if (createGroup) {
this.createNoteGroup(noteArray, "");
}
else {
let groupArray = this.getAnnotationsByType(AnnotationType.NOTE_GROUP);
if (groupArray.length > 0) {
for (let i = 0; i < groupArray.length; i++) {
if (groupArray[i].selected) {
if (deselectGroup) {
groupArray[i].deleteNoteArray(noteArray);
}
else {
groupArray[i].addNoteArray(noteArray);
}
if (this.onUpdate !== undefined) {
this.onUpdate(groupArray[i]);
}
}
}
}
}
this.drawNoteGroups();
}
}
}
/**
* Draw boundaries on this pane.
*/
drawBoundaries() {
if (this.showBoundaries) {
this.eraseBoundaries();
this.boundaryAnnotationsGroup = this.annotationsGroup.append("g")
.attr("class", "boundaryAnnotationsGroup");
let drawBoundaryArray = this.boundaryArray.filter(annotation => annotation.status !== AnnotationStatus.DELETED &&
annotation.time > this.timeScale.domain()[0] &&
annotation.time < this.timeScale.domain()[1]);
for (let i = 0; i < drawBoundaryArray.length; i++) {
drawBoundaryArray[i].draw(this.boundaryAnnotationsGroup, this.width, this.height, this.timeScale);
}
}
}
/**
* Erase boundaries from this pane.
*/
eraseBoundaries() {
for (let i = 0; i < this.boundaryArray.length; i++) {
this.boundaryArray[i].erase();
}
}
/**
* Draw comments on this pane.
*/
drawComments() {
if (this.showComments) {
this.eraseComments();
this.commentAnnotationsGroup = this.annotationsGroup.append("g")
.attr("class", "commentAnnotationsGroup");
let drawCommentArray = this.commentArray.filter(annotation => annotation.status !== AnnotationStatus.DELETED &&
annotation.time > this.timeScale.domain()[0] &&
annotation.time < this.timeScale.domain()[1]);
for (let i = 0; i < drawCommentArray.length; i++) {
drawCommentArray[i].draw(this.commentAnnotationsGroup, this.width, this.height, this.timeScale);
}
}
}
/**
* Erase comments from this pane.
*/
eraseComments() {
for (let i = 0; i < this.commentArray.length; i++) {
this.commentArray[i].erase();
}
}
/**
* Draw markers on this pane.
*/
drawMarkers() {
if (this.showMarkers) {
this.eraseMarkers();
this.markerAnnotationsGroup = this.annotationsGroup.append("g")
.attr("class", "markerAnnotationsGroup");
let drawMarkerArray = this.markerArray.filter(annotation => annotation.status !== AnnotationStatus.DELETED &&
annotation.time > this.timeScale.domain()[0] &&
annotation.time < this.timeScale.domain()[1]);
for (let i = 0; i < drawMarkerArray.length; i++) {
drawMarkerArray[i].draw(this.markerAnnotationsGroup, this.width, this.height, this.timeScale);
}
}
}
/**
* Erase markers from this pane.
*/
eraseMarkers() {
for (let i = 0; i < this.markerArray.length; i++) {
this.markerArray[i].erase();
}
}
/**
* Draw note selectors on this pane. Note selectors are invisible interactive rectangles that cover notes. When clicked
* a note selector can either add or remove a note from a note group or create a new note group.
*/
drawNoteSelectors() {
if (this.notes !== undefined && this.showNoteGroups) {
this.noteSelectorGroup = this.annotationsGroup.append("g")
.attr("class", "noteSelectorGroup");
this.noteSelectorGroup.selectAll("noteRect")
.data(this.notes.data)
.join("rect")
.attr("class", "annotation noteSelector")
.attr("x", d => this.notes.timeScale(d.startTime))
.attr("y", d => this.notes.noteScale(d.note))
.attr("width", (d) => {
let w = this.notes.timeScale(d.endTime) - this.notes.timeScale(d.startTime);
if (w > 0) {
return w;
}
else {
return 0;
}
})
.attr("height", () => this.notes.noteHeight)
.style("fill-opacity", "0")
.style("stroke-opacity", "0")
.style("stroke-width", this.notes.strokeWidth)
.style("pointer-events", "all")
.on("click", (event, d) => {
let createGroup = true;
if (event.metaKey || event.ctrlKey) {
createGroup = false;
}
this.selectNote(d, createGroup);
event.stopPropagation();
});
}
}
/**
* Draw note groups on this pane.
*/
drawNoteGroups() {
if (this.showNoteGroups) {
this.eraseNoteGroups();
this.noteGroupAnnotationsGroup = this.annotationsGroup.append("g")
.attr("class", "noteGroupAnnotationsGroup");
let noteHeight = this.height/88;
let drawUnselectedArray = this.noteGroupArray.filter(annotation => annotation.status !== AnnotationStatus.DELETED && !annotation.selected);
let drawSelectedArray = this.noteGroupArray.filter(annotation => annotation.status !== AnnotationStatus.DELETED && annotation.selected);
// Draw un-selected groups first
for (let i = 0; i < drawUnselectedArray.length; i++) {
drawUnselectedArray[i].draw(this.noteGroupAnnotationsGroup, this.timeScale, noteHeight);
}
// Draw selected groups last so that notes in multiple groups are shown properly when one is selected
for (let i = 0; i < drawSelectedArray.length; i++) {
drawSelectedArray[i].draw(this.noteGroupAnnotationsGroup, this.timeScale, noteHeight);
}
}
}
/**
* Erase note groups from this pane.
*/
eraseNoteGroups() {
for (let i = 0; i < this.noteGroupArray.length; i++) {
this.noteGroupArray[i].erase();
}
}
/**
* Draw regions on this pane.
*/
drawRegions() {
if (this.showRegions) {
this.eraseRegions();
this.regionAnnotationsGroup = this.annotationsGroup.append("g")
.attr("class", "regionAnnotationsGroup");
let drawRegionArray = this.regionArray.filter(annotation => annotation.status !== AnnotationStatus.DELETED);
for (let i = 0; i < drawRegionArray.length; i++) {
drawRegionArray[i].draw(this.regionAnnotationsGroup, this.width, this.height, this.timeScale);
}
}
}
/**
* Erase regions from this pane.
*/
eraseRegions() {
for (let i = 0; i < this.regionArray.length; i++) {
this.regionArray[i].erase();
}
}
/**
* Get the name string for the specified annotation type.
* @param {AnnotationType} type - The type of annotation.
*/
getAnnotationTypeName(type) {
var name;
switch(type) {
case AnnotationType.BOUNDARY:
name = "Boundaries";
break;
case AnnotationType.COMMENT:
name = "Comments";
break;
case AnnotationType.MARKER:
name = "Markers";
break;
case AnnotationType.NOTE_GROUP:
name = "Note Groups";
break;
case AnnotationType.REGION:
name = "Regions";
break;
}
return name;
}
/**
* Draw controls that affect selected annotations of any type.
*/
drawSelectedAnnotationControls() {
var selectedAnnotationControlDiv = this.annotationListDiv.append("div")
.attr("class", "selectedAnnotationControlDiv")
.style("display", "grid")
.style("grid-template-columns", "1fr 1fr")
.style("padding-bottom", "2ex")
.style("border-bottom", "1px solid black")
.style("margin-bottom", "2ex");
var selectAllDiv = selectedAnnotationControlDiv.append("div")
.attr("class", "selectAllDiv")
.style("display", "flex")
.style("flex-direction", "row");
selectAllDiv.append("div")
.attr("class", "selectAllCheckboxDiv")
.append("input")
.attr("type", "checkbox")
.attr("name", "selectAllCheckbox")
.on("change", (event) => {
switch(this.typeMode) {
case AnnotationType.BOUNDARY:
for (let i = 0; i < this.boundaryArray.length; i++) {
this.boundaryArray[i].select(event.target.checked);
}
this.drawBoundaryList();
break;
case AnnotationType.COMMENT:
for (let i = 0; i < this.commentArray.length; i++) {
this.commentArray[i].select(event.target.checked);
}
this.drawCommentList();
break;
case AnnotationType.MARKER:
for (let i = 0; i < this.markerArray.length; i++) {
this.markerArray[i].select(event.target.checked);
}
this.drawMarkerList();
break;
case AnnotationType.NOTE_GROUP:
for (let i = 0; i < this.noteGroupArray.length; i++) {
this.noteGroupArray[i].select(event.target.checked);
}
this.drawNoteGroupList();
break;
case AnnotationType.REGION:
for (let i = 0; i < this.regionArray.length; i++) {
this.regionArray[i].select(event.target.checked);
}
this.drawRegionList();
break;
}
});
if (this.typeMode === AnnotationType.BOUNDARY) {
selectAllDiv.append("div")
.attr("class", "selectedBoundaryStrengthDiv")
.style("margin-left", "0.5ch")
.append("select")
.attr("id", "selectedBoundaryStrengthSelect")
.on("change", (event) => {
for (let i = 0; i < this.boundaryArray.length; i++) {
if (this.boundaryArray[i].status !== AnnotationStatus.DELETED && this.boundaryArray[i].selected) {
this.boundaryArray[i].setStrength(parseInt(event.target.value));
if (this.onUpdate !== undefined) {
this.onUpdate(this.boundaryArray[i]);
}
}
}
this.draw();
this.drawAnnotationList();
})
.selectAll("option")
.data(this.boundaryStrengths)
.enter()
.append("option")
.attr("value", (d) => d)
.text((d) => d);
d3.select("#selectedBoundaryStrengthSelect").property("selectedIndex", "-1");
}
selectedAnnotationControlDiv.append("div")
.attr("class", "deleteSelectedDiv")
.style("display", "flex")
.style("flex-direction", "row")
.style("justify-content", "flex-end")
.append("button")
.attr("type", "button")
.style("font-weight", "bold")
.text("\u2715")
.on("click", () => {
this.deleteSelectedAnnotations();
});
}
/**
* Draw the annotation list for the current annotation type. Display a message if no annotations
* of the current type exist.
*/
drawAnnotationList() {
if (this.annotationListDiv !== undefined) {
this.annotationListDiv.remove();
}
this.annotationListDiv = this.annotationPanelDiv.append("div")
.attr("class", "annotationListDiv")
.style("display", "flex")
.style("flex-direction", "column");
switch(this.typeMode) {
case AnnotationType.BOUNDARY:
if (this.getAnnotationsByType(AnnotationType.BOUNDARY).length > 0) {
this.drawSelectedAnnotationControls();
this.drawBoundaryList();
}
else {
this.annotationListDiv.append("div")
.text("No boundaries added");
}
break;
case AnnotationType.COMMENT:
if (this.getAnnotationsByType(AnnotationType.COMMENT).length > 0) {
this.drawSelectedAnnotationControls();
this.drawCommentList();
}
else {
this.annotationListDiv.append("div")
.text("No comments added");
}
break;
case AnnotationType.MARKER:
if (this.getAnnotationsByType(AnnotationType.MARKER).length > 0) {
this.drawSelectedAnnotationControls();
this.drawMarkerList();
}
else {
this.annotationListDiv.append("div")
.text("No markers added");
}
break;
case AnnotationType.NOTE_GROUP:
if (this.getAnnotationsByType(AnnotationType.NOTE_GROUP).length > 0) {
this.drawSelectedAnnotationControls();
this.drawNoteGroupList();
}
else {
this.annotationListDiv.append("div")
.text("No note groups added");
}
break;
case AnnotationType.REGION:
if (this.getAnnotationsByType(AnnotationType.REGION).length > 0) {
this.drawSelectedAnnotationControls();
this.drawRegionList();
}
else {
this.annotationListDiv.append("div")
.text("No regions added");
}
break;
}
}
/**
* Erase any annotation list.
*/
eraseAnnotationList() {
if (this.annotationListDiv !== undefined) {
this.annotationListDiv.selectAll(".annotationDiv").remove();
}
}
/**
* Draw the boundary list.
*/
drawBoundaryList() {
this.eraseAnnotationList();
if (this.boundaryArray.length > 0) {
this.boundaryArray.sort((a,b) => a.time-b.time);
for (let i = 0; i < this.boundaryArray.length; i++) {
if (this.boundaryArray[i].status !== AnnotationStatus.DELETED) {
let boundaryControlDiv = this.annotationListDiv.append("div")
.attr("class", "annotationDiv boundaryControlDiv")
.style("display", "flex")
.style("flex-direction", "row")
.style("align-items", "center")
.style("padding-bottom", "1ex");
this.drawCheckBox(boundaryControlDiv, this.boundaryArray[i]);
this.drawStrengthSelect(boundaryControlDiv, this.boundaryArray[i]);
this.drawTime(boundaryControlDiv, this.boundaryArray[i]);
this.drawLabelInput(boundaryControlDiv, 15, this.boundaryArray[i]);
}
}
}
}
/**
* Draw the comment list.
*/
drawCommentList() {
this.eraseAnnotationList();
if (this.commentArray.length > 0) {
this.commentArray.sort((a,b) => a.time-b.time);
for (let i = 0; i < this.commentArray.length; i++) {
if (this.commentArray[i].status !== AnnotationStatus.DELETED) {
let commentControlDiv = this.annotationListDiv.append("div")
.attr("class", "annotationDiv commentControlDiv")
.style("display", "flex")
.style("flex-direction", "row")
.style("align-items", "center")
.style("padding-bottom", "1ex");
this.drawCheckBox(commentControlDiv, this.commentArray[i]);
this.drawTime(commentControlDiv, this.commentArray[i]);
this.drawLabelInput(commentControlDiv, 20, this.commentArray[i]);
}
}
}
}
/**
* Draw the marker list.
*/
drawMarkerList() {
this.eraseAnnotationList();
if (this.markerArray.length > 0) {
this.markerArray.sort((a,b) => a.time-b.time);
for (let i = 0; i < this.markerArray.length; i++) {
if (this.markerArray[i].status !== AnnotationStatus.DELETED) {
let markerControlDiv = this.annotationListDiv.append("div")
.attr("class", "annotationDiv markerControlDiv")
.style("display", "flex")
.style("flex-direction", "row")
.style("align-items", "center")
.style("padding-bottom", "1ex");
this.drawCheckBox(markerControlDiv, this.markerArray[i]);
this.drawTime(markerControlDiv, this.markerArray[i]);
this.drawLabelInput(markerControlDiv, 20, this.markerArray[i]);
}
}
}
}
/**
* Draw the note group list.
*/
drawNoteGroupList() {
this.eraseAnnotationList();
if (this.noteGroupArray.length > 0) {
for (let i = 0; i < this.noteGroupArray.length; i++) {
if (this.noteGroupArray[i].status !== AnnotationStatus.DELETED) {
let noteGroupControlDiv = this.annotationListDiv.append("div")
.attr("class", "annotationDiv noteGroupControlDiv")
.style("display", "flex")
.style("flex-direction", "row")
.style("align-items", "center")
.style("padding-bottom", "1ex");
this.drawCheckBox(noteGroupControlDiv, this.noteGroupArray[i]);
this.drawLabelInput(noteGroupControlDiv, 24, this.noteGroupArray[i]);
}
}
}
}
/**
* Draw the region list.
*/
drawRegionList() {
this.eraseAnnotationList();
if (this.regionArray.length > 0) {
this.regionArray.sort((a,b) => a.startTime-b.startTime);
for (let i = 0; i < this.regionArray.length; i++) {
if (this.regionArray[i].status !== AnnotationStatus.DELETED) {
let regionControlDiv = this.annotationListDiv.append("div")
.attr("class", "annotationDiv regionControlDiv")
.style("display", "flex")
.style("flex-direction", "row")
.style("align-items", "center")
.style("padding-bottom", "1ex");
this.drawCheckBox(regionControlDiv, this.regionArray[i]);
this.drawTime(regionControlDiv, this.regionArray[i]);
this.drawLabelInput(regionControlDiv, 12, this.regionArray[i]);
}
}
}
}
/**
* Draw a checkbox for selecting or deselecting the specified annotation.
* @param {d3.selection} parentElement - The parent element for this checkbox.
* @param {annotation} annotation - The annotation to control.
*/
drawCheckBox(parentElement, annotation) {
var annotationCheckboxDiv = parentElement.append("div")
.attr("class", "annotationCheckboxDiv");
if (annotation.active) {
annotationCheckboxDiv.append("input")
.attr("type", "checkbox")
.property("checked", () => {
if (annotation.selected) {
return true;
}
else {
return false;
}
})
.on("change", (event) => {
annotation.select(event.target.checked);
if (!event.target.checked) {
this.drawAnnotationList();
}
});
}
else {
annotationCheckboxDiv.append("input")
.attr("type", "checkbox")
.style("visibility", "hidden");
}
}
/**
* Draw a select list for setting the strength of a boundary.
* @param {d3.selection} parentElement - The parent element for this checkbox.
* @param {Boundary} annotation - The boundary whose strength should be set.
*/
drawStrengthSelect(parentElement, annotation) {
if (annotation.active) {
parentElement.append("div")
.attr("class", "boundaryStrengthDiv")
.style("padding-left", "1%")
.style("padding-right", "1%")
.append("select")
.attr("class", "boundaryStrengthSelect")
.on("change", (event, d) => {
annotation.setStrength(parseInt(event.target.value));
this.draw();
if (this.onUpdate !== undefined) {
this.onUpdate(annotation);
}
})
.selectAll("option")
.data(this.boundaryStrengths)
.enter()
.append("option")
.attr("value", (d) => d)
.property("selected", (d) => {
if (annotation.strength === d) {
return true;
}
else {
return false;
}
})
.text((d) => d);
}
else {
parentElement.append("div")
.attr("class", "boundaryStrengthDiv")
.append("select")
.attr("class", "boundaryStrengthSelect")
.append("option")
.attr("value", annotation.strength)
.attr("disabled", true)
.text(annotation.strength);
}
}
/**
* Draw the time value of the specified annotation.
* @param {d3.selection} parentElement - The parent element for this checkbox.
* @param {annotation} annotation - The time of the annotation.
*/
drawTime(parentElement, annotation) {
parentElement.append("div")
.attr("class", "annotationTimeDiv")
.style("text-align", "center")
.style("color", "#800000")
.style("white-space", "nowrap")
.style("padding-left", "1%")
.style("padding-right", "1%")
.text(() => {
if ("time" in annotation) {
let zeroPadding = "";
let roundedTime = (annotation.time%60).toFixed(2);
if (roundedTime < 10) {
zeroPadding = "0"
}
return (Math.floor(annotation.time/60)).toString() + ":" + zeroPadding + roundedTime.toString();
}
else {
let zeroPaddingStart = "";
let zeroPaddingEnd = "";
if (annotation.startTime%60 < 10) {
zeroPaddingStart = "0"
}
if (annotation.endTime%60 < 10) {
zeroPaddingEnd = "0"
}
return (Math.floor(annotation.startTime/60)).toString() + ":" + zeroPaddingStart + (annotation.startTime%60).toFixed(2) + " - "
+ (Math.floor(annotation.endTime/60)).toString() + ":" + zeroPaddingEnd + (annotation.endTime%60).toFixed(2);
}
});
}
/**
* Draw an input text box for setting the label of the specified annotation.
* @param {d3.selection} parentElement - The parent element for this checkbox.
* @param {number} size - The size of the input in characters.
* @param {annotation} annotation - The time of the annotation.
*/
drawLabelInput(parentElement, size, annotation) {
var labelInputDiv = parentElement.append("div")
.attr("class", "labelInputDiv")
.style("padding-left","1%")
.style("overflow-wrap", "break-word")
.append("input")
.attr("type", "text")
.attr("size", size)
.attr("value", annotation.label)
.style("font-size", "medium")
.on("input", (event) => {
annotation.setLabel(event.target.value);
if (this.onUpdateLabel !== undefined) {
this.onUpdateLabel();
}
})
.on("keydown", (event) => {
if (event.key === "1" ||
event.key === "2" ||
event.key === "3" ||
event.key === "4" ||
event.key === "Backspace" ||
event.key === "Delete" ||
event.key === "A" ||
event.key === "S") {
event.stopPropagation();
}
});
}
/**
* Activate the save annotation button.
*/
activateSaveButton() {
if (this.annotationSaveButton !== undefined) {
this.saveButtonActivated = true;
this.annotationSaveButton.property("disabled", false);
}
}
/**
* De-activate the save annotation button.
*/
deactivateSaveButton() {
if (this.annotationSaveButton !== undefined) {
this.saveButtonActivated = false;
this.annotationSaveButton.property("disabled", true);
}
}
/**
* Is the specified annotation type allowed for this pane? If an annotation type is not allowed, then no annotations of that type
* can be created or displayed.
* @param {AnnotationType} type - The type of annotation.
*/
isTypeAllowed(type) {
if (type === AnnotationType.BOUNDARY) {
return this.allowBoundaries;
}
else if (type === AnnotationType.COMMENT) {
return this.allowComments;
}
else if (type === AnnotationType.MARKER) {
return this.allowMarkers;
}
else if (type === AnnotationType.NOTE_GROUP) {
return this.allowNoteGroups;
}
else if (type === AnnotationType.REGION) {
return this.allowRegions;
}
}
/**
* Initialize the panel, attaching it to the specified parent element.
* @param {element} panelElement - The panel's parent element.
*/
initializePanel(panelElement) {
this.panelElement = panelElement instanceof d3.selection ? panelElement : d3.select(panelElement);
this.annotationPanelDiv = this.panelElement.append("div")
.attr("class", "annotationPanelDiv")
.style("width", this.panelWidth)
.style("display", "flex")
.style("flex-direction", "column")
.style("font-family", "sans-serif");
this.panelInitialized = true;
}
/**
* Draw the panel.
*/
drawPanel() {
if (this.panelInitialized) {
this.panelOn = true;
if (this.annotationPanelDiv !== undefined) {
this.annotationPanelDiv.selectAll("*").remove();
}
if (this.showPanelTitle) {
this.annotationPanelDiv.append("div")
.attr("class", "annotationTitleDiv")
.style("font-weight", "bold")
.style("font-size", "x-large")
.style("margin-bottom", "1ex")
.text("Annnotations");
}
this.annotationTypeDiv = this.annotationPanelDiv.append("div")
.attr("class", "annotationTypeDiv")
.style("display", "flex")
.style("flex-direction", "column")
.style("margin-bottom", "2ex")
.style("padding", "2ch")
.style("border", "1px solid black");
for (let i = 0; i < this.typeValues.length; i++) {
if (this.isTypeAllowed(this.typeValues[i])) {
let annotationTypeControlDiv = this.annotationTypeDiv.append("div")
.attr("class", "annotationTypeControlDiv")
.style("display", "flex")
.style("flex-direction", "row")
.style("align-items", "center");
annotationTypeControlDiv.append("div")
.attr("class", "annotationTypeRadio")
.append("input")
.attr("id", "radio" + this.typeValues[i])
.attr("type", "radio")
.attr("name", "annotationMode")
.attr("value", this.typeValues[i])
.property("checked", () => {
if (this.typeMode === this.typeValues[i]) {
return true;
}
else {
return false;
}
})
.on("change", (event) => {
this.setTypeMode(this.typeValues[i]);
this.erasePanel();
this.drawPanel();
});
annotationTypeControlDiv.append("div")
.attr("class", "annotationTypeCheckbox")
.append("input")
.attr("type", "checkbox")
.attr("id", "checkbox" + this.typeValues[i])
.property("checked", () => {
switch(this.typeValues[i]) {
case AnnotationType.BOUNDARY:
return this.showBoundaries;
break;
case AnnotationType.COMMENT:
return this.showComments;
break;
case AnnotationType.MARKER:
return this.showMarkers;
break;
case AnnotationType.NOTE_GROUP:
return this.showNoteGroups;
break;
case AnnotationType.REGION:
return this.showRegions;
break;
}
})
.on("change", (event) => {
this.showAnnotationType(this.typeValues[i], event.target.checked);
});
annotationTypeControlDiv.append("label")
.attr("class", "annotationTypeLabel")
.attr("for", "radio" + this.typeValues[i])
.style("vertical-align", "baseline")
.style("font-weight", () => {
if (this.typeMode === this.typeValues[i]) {
return "bold";
}
else {
return "normal";
}
})
.text(this.getAnnotationTypeName(this.typeValues[i]));
}
}
if (this.allowSaving) {
let annotationSaveDiv = this.annotationTypeDiv.append("div")
.attr("class", "annotationSaveDiv")
.style("display", "flex")
.style("flex-direction", "row")
.style("justify-content", "flex-start")
.style("margin-top", "1ex");
let annotationSaveButtonDiv = annotationSaveDiv.append("div")
.attr("class", "annotationSaveButtonDiv")
.style("width", "fit-content");
this.annotationSaveButton = annotationSaveButtonDiv.append("button")
.attr("type", "button")
.attr("id", "annotationSaveButton")
.style("font-weight", "bold")
.property("disabled", !this.saveButtonActivated)
.text("Save Annotations")
.on("click", () => {
this.deactivateSaveButton();
if (this.onSave !== undefined) {
this.onSave();
}
});
}
this.drawAnnotationList();
}
}
/**
* Erase the panel.
*/
erasePanel() {
if (this.annotationPanelDiv !== undefined) {
this.annotationPanelDiv.selectAll("*").remove();
this.panelOn = false;
}
}
/**
* Initialize this annotation pane, attaching it to the specified parent element and setting its size and time scale.
* @param {d3.selection} parentElement - The parent element.
* @param {number} width - The width.
* @param {number} height - The height.
* @param {d3.scaleLinear} timeScale - The time scale.
*/
initialize(parentElement, width, height, timeScale) {
this.parentElement = parentElement;
this.width = width;
this.height = height;
this.timeScale = timeScale;
// Drag events for creating regions or note groups
this.dragHandler = d3.drag()
.on("start", (event) => {
if (this.allowRegions && this.typeMode === AnnotationType.REGION && this.showRegions) {
let mouseX = d3.pointer(event)[0];
this.regionSelectRect = this.annotationsGroup.append("rect")
.attr("class", "regionSelectRect")
.attr("x", mouseX)
.attr("y", 0)
.attr("width", 0)
.attr("height", this.height)
.style("fill", this.annotationColor)
.style("fill-opacity", 0.2)
.style("stroke", this.annotationColor)
.style("stroke-width", 1);
}
else if (this.allowNoteGroups && this.typeMode === AnnotationType.NOTE_GROUP && this.showNoteGroups) {
this.groupSelectRect = this.annotationsGroup.append("rect")
.attr("class", "groupSelectRect")
.attr("x", event.x)
.attr("y", event.y)
.attr("width", 0)
.attr("height", 0)
.style("stroke", "#800000")
.style("stroke-width", "1")
.style("fill-opacity", "0");
}
})
.on("drag", (event) => {
let mouseX = d3.pointer(event)[0];
if (this.allowRegions && this.typeMode === AnnotationType.REGION && this.showRegions) {
let x = parseFloat(this.regionSelectRect.attr("x"));
let w = parseFloat(this.regionSelectRect.attr("width"));
if (event.dx !== 0 && (w+event.dx) > 0 && (x+w+event.dx < this.width)) {
this.regionSelectRect.attr("width", w+event.dx);
}
}
else if (this.allowNoteGroups && this.typeMode === AnnotationType.NOTE_GROUP && this.showNoteGroups) {
let w = parseFloat(this.groupSelectRect.attr("width"));
let h = parseFloat(this.groupSelectRect.attr("height"));
if ((w+event.dx >= 0) && (h+event.dy >= 0)) {
this.groupSelectRect
.attr("width", w+event.dx)
.attr("height", h+event.dy);
}
}
})
.on("end", (event) => {
if (this.allowRegions && this.typeMode === AnnotationType.REGION && this.showRegions) {
let w = parseFloat(this.regionSelectRect.attr("width"));
if (w > 0) {
if (w < 12) {
w = 12;
}
let x = parseFloat(this.regionSelectRect.attr("x"));
let t1 = this.timeScale.invert(x);
let t2 = this.timeScale.invert(x+w);
this.regionSelectRect.remove();
this.createRegion(t1, t2, "");
this.draw();
}
}
else if (this.allowNoteGroups && this.typeMode === AnnotationType.NOTE_GROUP && this.showNoteGroups) {
let createGroup = true;
let deselectGroup = false;
if (event.sourceEvent.metaKey || event.sourceEvent.ctrlKey) {
createGroup = false;
}
if (event.sourceEvent.shiftKey) {
deselectGroup = true;
createGroup = false;
}
let x = parseFloat(this.groupSelectRect.attr("x"));
let t1 = this.timeScale.invert(x);
let t2 = this.timeScale.invert(x+parseFloat(this.groupSelectRect.attr("width")));
let y1 = parseFloat(this.groupSelectRect.attr("y")) + parseFloat(this.groupSelectRect.attr("height"));
let y2 = parseFloat(this.groupSelectRect.attr("y"));
this.selectNoteRange(t1, t2, y1, y2, createGroup, deselectGroup);
this.groupSelectRect.remove();
}
if (this.panelOn) {
if (this.allowSaving) {
this.activateSaveButton();
}
this.drawAnnotationList();
}
});
this.annotationPaneGroup = this.parentElement.append("g")
.attr("class", "annotationPaneGroup");
this.annotationPaneGroup.append("rect")
.attr("class", "annotationPaneEventRect")
.attr("x", 0)
.attr("y", 0)
.attr("width", this.width)
.attr("height", this.height)
.style("fill", "none")
.style("pointer-events", "visible")
.style("cursor", "default");
this.annotationPaneGroup
.on("click", (event) => {
let pointerX = d3.pointer(event)[0];
switch (this.typeMode) {
case AnnotationType.BOUNDARY:
if (this.allowBoundaries && this.showBoundaries) {
this.createBoundary(this.timeScale.invert(pointerX), 4, "");
this.draw();
if (this.panelOn) {
if (this.allowSaving) {
this.activateSaveButton();
}
this.drawAnnotationList();
}
}
break;
case AnnotationType.COMMENT:
if (this.allowComments && this.showComments) {
this.createComment(this.timeScale.invert(pointerX), "");
this.draw();
if (this.panelOn) {
if (this.allowSaving) {
this.activateSaveButton();
}
this.drawAnnotationList();
}
}
break;
case AnnotationType.MARKER:
if (this.allowMarkers && this.showMarkers) {
this.createMarker(this.timeScale.invert(pointerX), "");
this.draw();
if (this.panelOn) {
if (this.allowSaving) {
this.activateSaveButton();
}
this.drawAnnotationList();
}
}
break;
}
})
.on("selectAnnotation", (event) => {
if (event.detail.soleSelection) {
this.#deselectOthers(event.detail.annotation);
}
if (this.panelOn) {
this.drawAnnotationList();
}
if (this.onSelect !== undefined) {
this.onSelect(event.detail.annotation);
}
})
.on("deselectAnnotation", (event) => {
if (this.panelOn) {
this.drawAnnotationList();
}
})
.on("updateAnnotation", (event) => {
if (this.panelOn) {
if (this.allowSaving) {
this.activateSaveButton();
}
this.drawAnnotationList();
}
if (this.onUpdate !== undefined) {
this.onUpdate(event.detail.annotation);
}
})
.on("deselectNote", (event) => {
if (this.onUpdate !== undefined) {
this.onUpdate(event.detail.annotation);
}
})
.call(this.dragHandler);
this.initialized = true;
}
/**
* Activate this pane, allowing it to accept pointer events.
*/
activate() {
if (this.initialized) {
this.annotationPaneGroup.style("pointer-events", "visible");
this.activateKeyboardInput();
}
}
/**
* Deactivate this pane, preventing it from accepting pointer events.
*/
deactivate() {
if (this.initialized) {
this.annotationPaneGroup.style("pointer-events", "none");
this.deactivateKeyboardInput()
}
}
/**
* Raise this pane to the top of the list of panes in its containing TimeFrame.
*/
raise() {
if (this.annotationPaneGroup !== undefined) {
this.annotationPaneGroup.raise();
}
}
/**
* Lower this pane to the bottom of the list of panes in its containing TimeFrame.
*/
lower() {
if (this.annotationPaneGroup !== undefined) {
this.annotationPaneGroup.lower();
}
}
/**
* Raise annotations of the specified type.
* @param {AnnotationType} type - The type of annotation.
*/
raiseAnnotations(type) {
switch (type) {
case AnnotationType.BOUNDARY:
if (this.boundaryAnnotationsGroup !== undefined) {
this.boundaryAnnotationsGroup.raise();
}
break;
case AnnotationType.COMMENT:
if (this.commentAnnotationsGroup !== undefined) {
this.commentAnnotationsGroup.raise();
}
break;
case AnnotationType.MARKER:
if (this.markerAnnotationsGroup !== undefined) {
this.markerAnnotationsGroup.raise();
}
break;
case AnnotationType.NOTE_GROUP:
if (this.noteGroupAnnotationsGroup !== undefined) {
this.noteGroupAnnotationsGroup.raise();
}
break;
case AnnotationType.REGION:
if (this.regionAnnotationsGroup !== undefined) {
this.regionAnnotationsGroup.raise();
}
break;
}
}
/**
* Activate keyboard controls for deleting annotations via the "Backspace" or "Delete" keys.
*/
activateKeyboardInput() {
d3.select("body").on("keydown.deleteAnnotations", (event) => {
if (event.key === "Backspace" || event.key === "Delete") {
event.preventDefault();
this.deleteSelectedAnnotations();
}
});
}
/**
* Deactivate keyboard controls.
*/
deactivateKeyboardInput() {
d3.select("body").on("keydown.deleteAnnotations", null);
}
/**
* Draw this annotation pane.
*/
draw() {
if (this.initialized) {
this.eraseAll();
this.annotationsGroup = this.annotationPaneGroup.append("g")
.attr("class", "annotationGroup")
.on("mouseenter", () => {
this.annotationsGroup.style("cursor", "pointer");
});
// Annotations (and note selectors) should be drawn in this order
this.drawNoteSelectors();
this.drawNoteGroups();
this.drawRegions();
this.drawComments();
this.drawMarkers();
this.drawBoundaries();
this.activateKeyboardInput();
}
}
erase() {
if (this.annotationPaneGroup !== undefined) {
this.annotationPaneGroup.remove();
}
}
/**
* Erase all annotations from this pane.
*/
eraseAll() {
if (this.annotationsGroup !== undefined) {
this.annotationsGroup.selectAll(".annotation").remove();
}
}
/**
* Get an array of all annotations of the specified type or an empty array if no annotations of that type exist.
* @param {AnnotationType} type - The type of annotation.
* @return {Array} An array of annotations or an empty array.
*/
getAnnotationsByType(type) {
var typeArray = [];
switch (type) {
case AnnotationType.BOUNDARY:
typeArray = this.boundaryArray.filter(annotation => annotation.status !== AnnotationStatus.DELETED);
break;
case AnnotationType.COMMENT:
typeArray = this.commentArray.filter(annotation => annotation.status !== AnnotationStatus.DELETED);
break;
case AnnotationType.MARKER:
typeArray = this.markerArray.filter(annotation => annotation.status !== AnnotationStatus.DELETED);
break;
case AnnotationType.NOTE_GROUP:
typeArray = this.noteGroupArray.filter(annotation => annotation.status !== AnnotationStatus.DELETED);
break;
case AnnotationType.REGION:
typeArray = this.regionArray.filter(annotation => annotation.status !== AnnotationStatus.DELETED);
break;
}
return typeArray;
}
/**
* Get an array of selected annotations of the specified type or an empty array if no annotations of that type are selected.
* @param {AnnotationType} type - The type of annotation.
* @return {Array} An array of selected annotations or an empty array.
*/
getSelectedAnnotationsByType(type) {
var selectedArray = [];
switch (type) {
case AnnotationType.BOUNDARY:
selectedArray = this.boundaryArray.filter(annotation => annotation.status !== AnnotationStatus.DELETED && annotation.selected === true);
break;
case AnnotationType.COMMENT:
selectedArray = this.commentArray.filter(annotation => annotation.status !== AnnotationStatus.DELETED && annotation.selected === true);
break;
case AnnotationType.MARKER:
selectedArray = this.markerArray.filter(annotation => annotation.status !== AnnotationStatus.DELETED && annotation.selected === true);
break;
case AnnotationType.NOTE_GROUP:
selectedArray = this.noteGroupArray.filter(annotation => annotation.status !== AnnotationStatus.DELETED && annotation.selected === true);
break;
case AnnotationType.REGION:
selectedArray = this.regionArray.filter(annotation => annotation.status !== AnnotationStatus.DELETED && annotation.selected === true);
break;
}
return selectedArray;
}
/**
* Get annotations that have been changed such that their AnnotationStatus value is not UNCHANGED. Returns an empty array if no
* annotations have been changed.
* @param {Array} annotationArray - An array of annotations.
* @return {Array} An array of changed annotations or an empty array.
*/
#getChangedAnnotationsByType(annotationArray) {
let changedAnnotations = [];
for (let i = 0; i < annotationArray.length; i++) {
if (annotationArray[i].status !== AnnotationStatus.UNCHANGED) {
changedAnnotations.push(annotationArray[i]);
}
}
return changedAnnotations;
}
/**
* Get annotations of all types that have been changed such that their AnnotationStatus value is not UNCHANGED. Returns an empty array if no
* annotations have been changed.
* @return {Array} An array of changed annotations or an empty array.
*/
getChangedAnnotations() {
return [...this.#getChangedAnnotationsByType(this.boundaryArray),
...this.#getChangedAnnotationsByType(this.commentArray),
...this.#getChangedAnnotationsByType(this.markerArray),
...this.#getChangedAnnotationsByType(this.noteGroupArray),
...this.#getChangedAnnotationsByType(this.regionArray)];
}
/**
* Get all annotations of all types or return an empty array if no annotations exist.
* @return {Array} An array of annotations or an empty array.
*/
getAllAnnotations() {
return [...this.boundaryArray,
...this.commentArray,
...this.markerArray,
...this.noteGroupArray,
...this.regionArray];
}
/**
* Get a count of all annotations in this pane.
* @return {number} The number of annotations.
*/
getAnnotationCount() {
return this.boundaryArray.length + this.commentArray.length + this.markerArray.length + this.noteGroupArray.length + this.regionArray.length;
}
/**
* Delete all selected annotations from the specified annotation array.
* @param {Array} annotationArray - An array of annotations.
*/
#deleteSelected(annotationArray) {
if (annotationArray.length > 0) {
// Iterate backwards to allow for splice while in loop
for (let i = annotationArray.length-1; i >= 0; --i) {
if (annotationArray[i].selected) {
annotationArray[i].erase();
if (this.onDelete !== undefined) {
this.onDelete(annotationArray[i]);
}
if (annotationArray[i].status === AnnotationStatus.CREATED) {
annotationArray.splice(i, 1);
}
else {
annotationArray[i].status = AnnotationStatus.DELETED;
}
}
}
}
}
/**
* Delete selected annotations of any type.
*/
deleteSelectedAnnotations() {
this.#deleteSelected(this.boundaryArray);
this.#deleteSelected(this.commentArray);
this.#deleteSelected(this.markerArray);
this.#deleteSelected(this.noteGroupArray);
this.#deleteSelected(this.regionArray);
if (this.panelOn) {
if (this.allowSaving) {
this.activateSaveButton();
}
this.drawAnnotationList();
}
}
/**
* Delete all annotations from the specified annotation array.
* @param {Array} annotationArray - An array of annotations.
*/
#deleteAll(annotationArray) {
if (annotationArray.length > 0) {
// Iterate backwards to allow for splice while in loop
for (let i = annotationArray.length-1; i >= 0; --i) {
if (this.onDelete !== undefined) {
this.onDelete(annotationArray[i]);
}
if (annotationArray[i].status === AnnotationStatus.CREATED) {
annotationArray.splice(i, 1);
}
else {
annotationArray[i].status = AnnotationStatus.DELETED;
}
}
}
}
/**
* Delete all annotations from this pane.
*/
deleteAllAnnotations() {
this.#deleteAll(this.boundaryArray);
this.#deleteAll(this.commentArray);
this.#deleteAll(this.markerArray);
this.#deleteAll(this.noteGroupArray);
this.#deleteAll(this.regionArray);
if (this.panelOn) {
if (this.allowSaving) {
this.activateSaveButton();
}
this.drawAnnotationList();
}
}
}
/** AnnotationStatus contains static fields for the status of annotations. */
class AnnotationStatus {
static UNCHANGED = 0;
static CREATED = 1;
static UPDATED = 2;
static DELETED = 3;
}
/** AnnotationType contains static fields representing annotation types. */
class AnnotationType {
static NONE = -1;
static BOUNDARY = 0;
static COMMENT = 1;
static MARKER = 2;
static NOTE_GROUP = 3;
static REGION = 4;
}
/** A boundary annotation. */
class Boundary {
// Sides
static LEFT = 0;
static RIGHT = 1;
/**
* Create a boundary annotation.
* @param {number} time - The time.
* @param {number} strength - The strength.
* @param {string} label - A label.
* @param {Object} options - Configuration options.
* @param {Object} properties - Properties.
* @param {boolean} active - Whether this boundary should be active when created.
* @param {boolean} firstCreated - Whether this boundary has just been created.
* @author Lawrence Fyfe
*/
constructor(time, strength, label, options, properties, active, firstCreated) {
this.type = "boundary";
this.status = AnnotationStatus.UNCHANGED;
this.selected = false;
this.firstCreated = firstCreated;
this.time = time;
this.strength = strength;
this.label = label;
this.x;
this.dx = 0;
this.color = options?.color ?? "#800000";
this.selectedColor = options?.selectedColor ?? "#bb0000";
this.alwaysShowLabel = options?.alwaysShowLabel ?? false;
this.properties = properties;
this.active = active;
this.show = true;
this.dateCreated;
this.dateUpdated;
this.boundaryGroup;
this.line;
this.clickLine;
this.triangleSide = 30;
this.triangleOffset = 4;
this.leftTriangleX;
this.leftTriangleTip;
this.rightTriangleX;
this.rightTriangleTip;
if (this.firstCreated) {
this.status = AnnotationStatus.CREATED;
this.dateCreated = new Date().toISOString();
}
}
/**
* Draw this boundary with the specified parameters from the containing pane.
* @param {d3.selection} parentElement - The parent element.
* @param {number} parentWidth - The width.
* @param {number} parentHeight - The height.
* @param {d3.scaleLinear} timeScale - The time scale.
*/
draw(parentElement, parentWidth, parentHeight, timeScale) {
if (this.show) {
this.eraseLabel();
this.x = timeScale(this.time);
this.boundaryGroup = parentElement.append("g")
.attr("class", "annotation boundaryGroup");
this.line = this.boundaryGroup.append("line")
.attr("class", "boundaryLine")
.attr("x1", this.x)
.attr("y1", 0)
.attr("x2", this.x)
.attr("y2", parentHeight)
.style("stroke", () => {
if (this.selected) {
return this.selectedColor;
}
else {
return this.color;
}
})
.style("stroke-width", this.getStrengthWidth())
.style("opacity", this.getStrengthOpacity());
this.clickLine = this.boundaryGroup.append("line")
.attr("class", "boundaryClickLine")
.attr("x1", this.x)
.attr("y1", 0)
.attr("x2", this.x)
.attr("y2", parentHeight)
.style("stroke", "#00000000")
.style("stroke-width", this.getStrengthClickWidth())
.call(d3.drag()
.on("drag", (event) => {
if (this.active && event.dx !== 0) {
this.dx += event.dx;
this.move(parentWidth, parentHeight, event.dx);
this.drawLabel(this.boundaryGroup, parentWidth, parentHeight, this.x);
}
})
.on("end", (event) => {
if (this.active && this.dx !== 0) {
this.dx = 0;
this.time = timeScale.invert(this.x);
if (!this.firstCreated) {
this.status = AnnotationStatus.UPDATED;
this.dateUpdated = new Date().toISOString();
}
event.sourceEvent.target.dispatchEvent(new CustomEvent("updateAnnotation", {bubbles: true, detail: {annotation: this}}));
}
}))
.on("mouseenter", () => {
this.boundaryGroup.raise();
this.drawLabel(this.boundaryGroup, parentWidth, parentHeight, this.x);
})
.on("mouseleave", (event) => {
this.eraseLabel();
})
.on("click", (event) => {
if (this.active) {
if (!this.selected) {
let soleSelection = true;
this.select(true);
if (event.metaKey || event.ctrlKey) {
soleSelection = false;
}
event.target.dispatchEvent(new CustomEvent("selectAnnotation", {bubbles: true, detail: {annotation: this, soleSelection: soleSelection}}));
}
else {
this.select(false);
event.target.dispatchEvent(new CustomEvent("deselectAnnotation", {bubbles: true}));
}
}
event.stopPropagation();
});
this.drawHandle(parentElement, parentWidth, parentHeight, timeScale, Boundary.LEFT);
this.drawHandle(parentElement, parentWidth, parentHeight, timeScale, Boundary.RIGHT);
if (this.selected) {
this.showHandles();
}
}
}
/**
* Erase this boundary.
*/
erase() {
if (this.boundaryGroup !== undefined) {
this.boundaryGroup.remove();
}
}
/**
* Draw the triangular handles for this boundary with the specified parameters.
* @param {d3.selection} parentElement - The parent element.
* @param {number} parentWidth - The width.
* @param {number} parentHeight - The height.
* @param {d3.scaleLinear} timeScale - The time scale.
* @param {number} direction - The RIGHT or the LEFT handle.
*/
drawHandle(parentElement, parentWidth, parentHeight, timeScale, direction) {
var handle = this.boundaryGroup.append("polygon")
.attr("class", () => {
if (direction === Boundary.LEFT) {
return "boundaryHandle leftHandle";
}
else if (direction === Boundary.RIGHT) {
return "boundaryHandle rightHandle";
}
})
.attr("points", () => {
if (direction === Boundary.LEFT) {
return this.getLeftHandlePoints();
}
else if (direction === Boundary.RIGHT) {
return this.getRightHandlePoints();
}
})
.style("visibility", "hidden")
.style("fill", this.selectedColor)
.style("opacity", this.getStrengthOpacity())
.on("click", (event) => {
if (this.active && this.selected) {
this.select(false);
event.target.dispatchEvent(new CustomEvent("deselectAnnotation", {bubbles: true}));
}
event.stopPropagation();
})
.call(d3.drag()
.on("drag", (event) => {
if (this.active && event.dx !== 0) {
this.dx += event.dx;
this.move(parentWidth, parentHeight, event.dx);
this.drawLabel(this.boundaryGroup, parentWidth, parentHeight, this.x);
}
})
.on("end", (event) => {
if (this.active && this.dx !== 0) {
this.dx = 0;
this.time = timeScale.invert(this.x);
if (!this.firstCreated) {
this.status = AnnotationStatus.UPDATED;
this.dateUpdated = new Date().toISOString();
}
event.sourceEvent.target.dispatchEvent(new CustomEvent("updateAnnotation", {bubbles: true, detail: {annotation: this}}));
}
}));
}
/**
* Draw the triangular handles for this boundary with the specified parameters.
* @param {d3.selection} parentElement - The parent element.
* @param {number} parentWidth - The width.
* @param {number} parentHeight - The height.
* @param {d3.scaleLinear} timeScale - The time scale.
* @param {number} x - The x location of this boundary.
*/
drawLabel(parentElement, parentWidth, parentHeight, x) {
this.eraseLabel();
var textAnchor;
if (x < parentWidth/2) {
x = x + this.getStrengthClickWidth()/2 + 4;
textAnchor = "start";
}
else {
x = x - this.getStrengthClickWidth()/2 - 4;
textAnchor = "end";
}
var labelGroup = parentElement.append("g")
.attr("class", "boundaryLabel");
var labelText = labelGroup.append("text")
.attr("x", x)
.attr("y", parentHeight-16)
.style("color", "black")
.style("text-anchor", textAnchor)
.style("dominant-baseline", "middle")
.text(this.label);
var textBox = labelText.node().getBBox();
var labelRect = labelGroup.append("rect")
.attr("x", textBox.x-2)
.attr("y", textBox.y-2)
.attr("width", textBox.width+4)
.attr("height", textBox.height+4)
.style("fill", "white");
labelText.raise();
}
/**
* Erase the label.
*/
eraseLabel() {
if (this.boundaryGroup !== undefined) {
this.boundaryGroup.selectAll(".boundaryLabel").remove();
}
}
/**
* Get the points for creating the left triangular handle.
* @return {points} Three points.
*/
getLeftHandlePoints() {
this.leftTriangleX = this.x-this.triangleOffset-(parseFloat(this.getStrengthWidth())/2);
this.leftTriangleTip = this.leftTriangleX-this.triangleSide;
return this.leftTriangleX + ",0 " + this.leftTriangleTip + "," + this.triangleSide/2 + " " + this.leftTriangleX + "," + this.triangleSide;
}
/**
* Get the points for creating the right triangular handle.
* @return {points} Three points.
*/
getRightHandlePoints() {
this.rightTriangleX = this.x+this.triangleOffset+(parseFloat(this.getStrengthWidth())/2);
this.rightTriangleTip = this.rightTriangleX+this.triangleSide;
return this.rightTriangleX + ",0 " + this.rightTriangleTip + "," + this.triangleSide/2 + " " + this.rightTriangleX + "," + this.triangleSide;
}
/**
* Select or deselect this boundary.
* @param {boolean} select - If true, select; otherwise deselect.
*/
select(select) {
this.selected = select;
if (this.selected) {
this.line
.style("stroke", this.selectedColor)
.style("stroke-width", this.getStrengthWidth());
this.showHandles();
}
else {
this.line
.style("stroke", this.color)
.style("stroke-width", this.getStrengthWidth());
this.hideHandles();
}
}
/**
* Show handles.
*/
showHandles() {
this.boundaryGroup.selectAll(".boundaryHandle").style("visibility", "visible");
}
/**
* Hide handles.
*/
hideHandles() {
this.boundaryGroup.selectAll(".boundaryHandle").style("visibility", "hidden");
}
/**
* Move this boundary to the specified x location.
* @param {number} parentWidth - The parent width.
* @param {number} parentHeight - The parent height.
* @param {number} moveX - The x location.
*/
move(parentWidth, parentHeight, moveX) {
let newX = this.x+moveX;
if (newX < 0) {
this.x = 0;
}
else if (newX > parentWidth) {
this.x = parentWidth;
}
else {
this.x = newX;
}
this.line
.attr("x1", this.x)
.attr("x2", this.x);
this.clickLine
.attr("x1", this.x)
.attr("x2", this.x);
this.boundaryGroup.selectAll(".leftHandle").attr("points", this.getLeftHandlePoints());
this.boundaryGroup.selectAll(".rightHandle").attr("points", this.getRightHandlePoints());
}
/**
* Set the strength.
* @param {number} strength - The strength value.
*/
setStrength(strength) {
this.strength = strength;
if (!this.firstCreated) {
this.status = AnnotationStatus.UPDATED;
this.dateUpdated = new Date().toISOString();
}
}
/**
* Set the label string.
* @param {string} label - The label.
*/
setLabel(label) {
this.label = label;
if (!this.firstCreated) {
this.status = AnnotationStatus.UPDATED;
this.dateUpdated = new Date().toISOString();
}
}
/**
* Get the line width based on the strength value.
* @return {string} The width as a string.
*/
getStrengthWidth() {
if (this.strength === 1) {
return "2";
}
else if (this.strength === 2) {
return "3";
}
else if (this.strength === 3) {
return "4";
}
else if (this.strength === 4) {
return "5";
}
}
/**
* Get the click width based on the strength value. The click width is always larger than the line width.
* @return {string} The width as a string.
*/
getStrengthClickWidth() {
if (this.strength === 1) {
return "13";
}
else if (this.strength === 2) {
return "14";
}
else if (this.strength === 3) {
return "15";
}
else if (this.strength === 4) {
return "16";
}
}
/**
* Get the line opacity based on the strength value.
* @return {string} The opacity as a string.
*/
getStrengthOpacity() {
if (this.strength === 1) {
return "0.3";
}
else if (this.strength === 2) {
return "0.5";
}
else if (this.strength === 3) {
return "0.7";
}
else if (this.strength === 4) {
return "1.0";
}
}
/**
* Get the time.
* @return {number} The time.
*/
getTime() {
return this.time;
}
}
/** A comment annotation. */
class Comment {
/**
* Create a comment annotation.
* @param {number} time - The time.
* @param {string} label - A label.
* @param {Object} options - Configuration options.
* @param {Object} properties - Properties.
* @param {boolean} active - Whether this comment should be active when created.
* @param {boolean} firstCreated - Whether this comment has just been created.
* @author Lawrence Fyfe
*/
constructor(time, label, options, properties, active, firstCreated) {
this.type = "comment";
this.status = AnnotationStatus.UNCHANGED;
this.selected = false;
this.deselected = false;
this.firstCreated = firstCreated;
this.time = time;
this.label = label;
this.x;
this.dx = 0;
this.color = options?.color ?? "#800000";
this.selectedColor = options?.selectedColor ?? "#bb0000";
this.alwaysShowLabel = options?.alwaysShowLabel ?? false;
this.properties = properties;
this.active = active;
this.show = true;
this.dateCreated;
this.dateUpdated;
this.commentGroup;
this.commentLine;
this.commentClickLine;
this.labelGroup;
if (this.firstCreated) {
this.status = AnnotationStatus.CREATED;
this.dateCreated = new Date().toISOString();
}
}
/**
* Draw this comment with the specified parameters from the containing pane.
* @param {d3.selection} parentElement - The parent element.
* @param {number} parentWidth - The width.
* @param {number} parentHeight - The height.
* @param {d3.scaleLinear} timeScale - The time scale.
*/
draw(parentElement, parentWidth, parentHeight, timeScale) {
if (this.show) {
this.eraseLabel();
this.x = timeScale(this.time);
this.commentGroup = parentElement.append("g")
.attr("class", "annotation commentGroup");
this.commentLine = this.commentGroup.append("line")
.attr("class", "commentLine")
.attr("x1", this.x)
.attr("y1", 0)
.attr("x2", this.x)
.attr("y2", parentHeight)
.style("stroke", () => {
if (this.selected) {
return this.selectedColor;
}
else {
return this.color;
}
})
.style("stroke-width", () => {
if (this.selected) {
return 8;
}
else {
return 4;
}
})
.style("stroke-linecap", "round")
.style("stroke-dasharray", "1 12");
this.commentClickLine = this.commentGroup.append("line")
.attr("class", "commentClickLine")
.attr("x1", this.x)
.attr("y1", 0)
.attr("x2", this.x)
.attr("y2", parentHeight)
.style("stroke", "#000000")
.style("stroke-opacity", "0")
.style("stroke-width", "15")
.call(d3.drag()
.on("drag", (event) => {
if (this.active && event.dx !== 0) {
this.dx += event.dx;
let newX = event.x;
if (newX < 0) {
newX = 0;
}
if (newX > parentWidth) {
newX = parentWidth;
}
this.x = newX;
this.commentClickLine
.attr("x1", this.x)
.attr("x2", this.x)
.style("cursor", "move");
this.commentLine
.attr("x1", this.x)
.attr("x2", this.x);
this.drawLabel(this.commentGroup, parentWidth, parentHeight, this.x);
}
})
.on("end", (event) => {
if (this.active && this.dx !== 0) {
this.dx = 0;
this.time = timeScale.invert(this.x);
if (!this.firstCreated) {
this.status = AnnotationStatus.UPDATED;
this.dateUpdated = new Date().toISOString();
}
event.sourceEvent.target.dispatchEvent(new CustomEvent("updateAnnotation", {bubbles: true, detail: {annotation: this}}));
this.commentClickLine.style("cursor", "pointer");
}
}))
// Mouse events
.on("mouseenter", (event) => {
this.commentGroup.raise();
this.drawLabel(this.commentGroup, parentWidth, parentHeight, this.x);
})
.on("mouseleave", (event) => {
this.eraseLabel();
})
.on("click", (event) => {
if (this.active) {
if (!this.selected) {
let soleSelection = true;
this.select(true);
if (event.metaKey || event.ctrlKey) {
soleSelection = false;
}
event.target.dispatchEvent(new CustomEvent("selectAnnotation", {bubbles: true, detail: {annotation: this, soleSelection: soleSelection}}));
}
else {
this.select(false);
event.target.dispatchEvent(new CustomEvent("deselectAnnotation", {bubbles: true}));
}
}
event.stopPropagation();
});
}
}
/**
* Erase this comment.
*/
erase() {
if (this.commentGroup !== undefined) {
this.commentGroup.remove();
}
}
/**
* Set the label string.
* @param {string} label - The label.
*/
setLabel(label) {
this.label = label;
if (!this.firstCreated) {
this.status = AnnotationStatus.UPDATED;
this.dateUpdated = new Date().toISOString();
}
}
/**
* Draw the label.
* @param {d3.selection} parentElement - The parent element.
* @param {number} parentWidth - The width.
* @param {number} parentHeight - The height.
* @param {number} x - The x location.
*/
drawLabel(parentElement, parentWidth, parentHeight, x) {
this.eraseLabel();
var textAnchor;
if (x < parentWidth/2) {
x = x + parseFloat(this.commentLine.style("stroke-width"));
textAnchor = "start";
}
else {
x = x - parseFloat(this.commentLine.style("stroke-width"));
textAnchor = "end";
}
this.labelGroup = this.commentGroup.append("g")
.attr("class", "labelGroup")
.attr("transform", "translate(" + x + ", " + 1 + ")");
var labelText = this.labelGroup.append("text")
.attr("x", 0)
.attr("y", parentHeight-16)
.style("font-family", "sans-serif")
.style("color", "black")
.style("text-anchor", textAnchor)
.style("dominant-baseline", "middle")
.text(this.label);
var textBox = labelText.node().getBBox();
var labelRect = this.labelGroup.append("rect")
.attr("x", textBox.x-2)
.attr("y", textBox.y-2)
.attr("width", textBox.width+4)
.attr("height", textBox.height+4)
.style("fill", "white");
labelText.raise();
}
/**
* Erase the label.
*/
eraseLabel() {
if (this.labelGroup !== undefined) {
this.labelGroup.remove();
}
}
/**
* Select or deselect this comment.
* @param {boolean} select - If true, select; otherwise deselect.
*/
select(select) {
this.selected = select;
if (this.selected) {
this.commentLine
.style("stroke", this.selectedColor)
.style("stroke-width", 8);
}
else {
this.commentLine
.style("stroke", this.color)
.style("stroke-width", 4);
}
}
/**
* Get the time.
* @return {number} The time.
*/
getTime() {
return this.time;
}
}
/** A marker annotation. */
class Marker {
/**
* Create a marker annotation.
* @param {number} time - The time.
* @param {string} label - A label.
* @param {Object} options - Configuration options.
* @param {Object} properties - Properties.
* @param {boolean} active - Whether this marker should be active when created.
* @param {boolean} firstCreated - Whether this marker has just been created.
* @author Lawrence Fyfe
*/
constructor(time, label, options, properties, active, firstCreated) {
this.type = "marker";
this.status = AnnotationStatus.UNCHANGED;
this.time = time;
this.label = label;
this.color = options?.color ?? "#800000";
this.width = options?.width ?? 2;
this.opacity = options?.opacity ?? "1.0";
this.alwaysShowLabel = options?.alwaysShowLabel ?? false;
this.properties = properties;
this.active = active;
this.show = true;
this.firstCreated = firstCreated;
this.dateCreated;
this.dateUpdated;
this.selected = false;
this.x;
this.dx = 0;
this.markerGroup;
this.line;
this.clickLine;
if (this.firstCreated) {
this.status = AnnotationStatus.CREATED;
this.dateCreated = new Date().toISOString();
}
}
/**
* Draw this marker with the specified parameters from the containing pane.
* @param {d3.selection} parentElement - The parent element.
* @param {number} parentWidth - The width.
* @param {number} parentHeight - The height.
* @param {d3.scaleLinear} timeScale - The time scale.
*/
draw(parentElement, parentWidth, parentHeight, timeScale) {
if (this.show) {
this.eraseLabel();
this.x = timeScale(this.time);
this.markerGroup = parentElement.append("g")
.attr("class", "annotation markerGroup");
this.line = this.markerGroup.append("line")
.attr("class", "markerLine")
.attr("x1", this.x)
.attr("y1", 0)
.attr("x2", this.x)
.attr("y2", parentHeight)
.style("stroke", this.color)
.style("stroke-width", () => {
if (this.selected) {
return 2*this.width;
}
else {
return this.width;
}
})
.style("opacity", this.opacity);
this.clickLine = this.markerGroup.append("line")
.attr("class", "markerClickLine")
.attr("x1", this.x)
.attr("y1", 0)
.attr("x2", this.x)
.attr("y2", parentHeight)
.style("stroke", "#00000000")
.style("stroke-width", 8*this.width)
.call(d3.drag()
.on("drag", (event) => {
if (this.active && event.dx !== 0) {
this.dx += event.dx;
this.move(parentWidth, event.dx);
this.drawLabel(this.markerGroup, parentWidth, parentHeight);
}
})
.on("end", (event) => {
if (this.active && this.dx !== 0) {
this.dx = 0;
this.time = timeScale.invert(this.x);
if (!this.firstCreated) {
this.status = AnnotationStatus.UPDATED;
this.dateUpdated = new Date().toISOString();
}
event.sourceEvent.target.dispatchEvent(new CustomEvent("updateAnnotation", {bubbles: true, detail: {annotation: this}}));
}
}))
.on("mouseenter", () => {
if (this.active) {
this.markerGroup.raise();
if (!this.alwaysShowLabel) {
this.drawLabel(this.markerGroup, parentWidth, parentHeight);
}
}
})
.on("mouseleave", (event) => {
if (this.active) {
if (!this.alwaysShowLabel) {
this.eraseLabel();
}
}
})
.on("click", (event) => {
if (this.active) {
if (!this.selected) {
let soleSelection = true;
this.select(true);
if (event.metaKey || event.ctrlKey) {
soleSelection = false;
}
event.target.dispatchEvent(new CustomEvent("selectAnnotation", {bubbles: true, detail: {annotation: this, soleSelection: soleSelection}}));
}
else {
this.select(false);
event.target.dispatchEvent(new CustomEvent("deselectAnnotation", {bubbles: true}));
}
}
event.stopPropagation();
});
if (this.alwaysShowLabel) {
this.drawLabel(this.markerGroup, parentWidth, parentHeight);
}
}
}
/**
* Erase this marker.
*/
erase() {
if (this.markerGroup !== undefined) {
this.markerGroup.remove();
}
}
/**
* Draw the label.
* @param {d3.selection} parentElement - The parent element.
* @param {number} parentWidth - The width.
* @param {number} parentHeight - The height.
*/
drawLabel(parentElement, parentWidth, parentHeight) {
this.eraseLabel();
var textAnchor;
var labelGroup = parentElement.append("g")
.attr("class", "markerLabel");
var labelText = labelGroup.append("text")
.attr("x", this.x)
.attr("y", "2ex")
.style("font-family", "sans-serif")
.style("color", "black")
.style("text-anchor", "middle")
.style("dominant-baseline", "middle")
.text(this.label);
var textBox = labelText.node().getBBox();
var labelRect = labelGroup.append("rect")
.attr("x", textBox.x-2)
.attr("y", textBox.y-2)
.attr("width", textBox.width+4)
.attr("height", textBox.height+4)
.style("fill", "white");
labelText.raise();
}
/**
* Erase the label.
*/
eraseLabel() {
if (this.markerGroup !== undefined) {
this.markerGroup.selectAll(".markerLabel").remove();
}
}
/**
* Select or deselect this marker.
* @param {boolean} select - If true, select; otherwise deselect.
*/
select(select) {
this.selected = select;
if (this.line !== undefined) {
if (this.selected) {
this.line.style("stroke-width", 3*this.width);
}
else {
this.line.style("stroke-width", this.width);
}
}
}
/**
* Move this marker by the specified amount.
* @param {number} parentWidth - The maximum value for the x location.
* @param {number} moveX - The change in the x location.
*/
move(parentWidth, moveX) {
let newX = this.x+moveX;
if (newX < 0) {
this.x = 0;
}
else if (newX > parentWidth) {
this.x = parentWidth;
}
else {
this.x = newX;
}
this.line
.attr("x1", this.x)
.attr("x2", this.x);
this.clickLine
.attr("x1", this.x)
.attr("x2", this.x);
}
/**
* Set the label string.
* @param {string} label - The label.
*/
setLabel(label) {
this.label = label;
if (!this.firstCreated) {
this.status = AnnotationStatus.UPDATED;
this.dateUpdated = new Date().toISOString();
}
}
/**
* Get the time.
* @return {number} The time.
*/
getTime() {
return this.time;
}
}
/** A note group annotation. */
class NoteGroup {
/**
* Create a marker annotation.
* @param {Array} noteArray - An array of notes.
* @param {Array} noteScale - The value scale for the notes.
* @param {string} label - A label.
* @param {Object} options - Configuration options.
* @param {Object} properties - Properties.
* @param {boolean} active - Whether this boundary should be active when created.
* @param {boolean} firstCreated - Whether this boundary has just been created.
* @author Lawrence Fyfe
*/
constructor(noteArray, noteScale, label, options, properties, active, firstCreated) {
this.type = "noteGroup";
this.status = AnnotationStatus.UNCHANGED;
this.selected = false;
this.selectionArray = [];
this.noteScale = noteScale;
this.firstCreated = firstCreated;
this.label = label;
this.options = options;
this.color = options?.color ?? "#800000";
this.selectedColor = options?.selectedColor ?? "#ff0000";
this.properties = properties;
this.active = active;
this.show = true;
this.dateCreated;
this.dateUpdated;
this.noteGroupGroup;
if (this.firstCreated) {
this.status = AnnotationStatus.CREATED;
this.dateCreated = new Date().toISOString();
this.selected = true;
}
if (noteArray !== undefined) {
this.#addSelectedNoteArray(noteArray);
}
}
/**
* Add the specified note selection.
* @param {NoteGroupSelection} s - The selected note.
*/
#addSelection(s) {
this.selectionArray.push(s);
}
/**
* Add the specified note array.
* @param {Array} noteArray - An array of notes.
*/
#addSelectedNoteArray(noteArray) {
if (noteArray.length > 0) {
for (let i = 0; i < noteArray.length; i++) {
this.#addSelection(new NoteGroupSelection(noteArray[i].note, noteArray[i].startTime, noteArray[i].endTime, this.options, this.selected));
}
}
}
/**
* Add the specified note to this group.
* @param {Object} note - The note.
*/
addNote(note) {
this.#addSelection(new NoteGroupSelection(note.note, note.startTime, note.endTime, this.options, this.selected));
if (!this.firstCreated) {
this.status = AnnotationStatus.UPDATED;
this.dateUpdated = new Date().toISOString();
}
}
/**
* Add the specified array of notes to this group.
* @param {Array} noteArray - An array of notes.
*/
addNoteArray(noteArray) {
if (noteArray.length > 0) {
for (let i = 0; i < noteArray.length; i++) {
if (!this.containsNote(noteArray[i].note, noteArray[i].startTime, noteArray[i].endTime)) {
this.#addSelection(new NoteGroupSelection(noteArray[i].note, noteArray[i].startTime, noteArray[i].endTime, this.options, this.selected));
}
}
if (!this.firstCreated) {
this.status = AnnotationStatus.UPDATED;
this.dateUpdated = new Date().toISOString();
}
}
}
/**
* Determine whether this group contains the specified note.
* @param {Object} note - A note.
* @return {boolean} True if this group contains the note; false otherwise.
*/
containsNote(note, startTime, endTime) {
var note = this.selectionArray.find(selection => selection.note === note && selection.startTime === startTime && selection.endTime === endTime);
if (note !== undefined) {
return true;
}
else {
return false;
}
}
/**
* Delete the specified array of notes from this group.
* @param {Array} noteArray - An array of notes.
*/
deleteNoteArray(noteArray) {
if (noteArray.length > 0) {
for (let i = 0; i < noteArray.length; i++) {
this.#deleteSelection(noteArray[i]);
}
if (!this.firstCreated) {
this.status = AnnotationStatus.UPDATED;
this.dateUpdated = new Date().toISOString();
}
}
}
/**
* Delete the specified note selection from this group.
* @param {NoteGroupSelection} selection - A note selection.
*/
#deleteSelection(selection) {
for (let i = this.selectionArray.length-1; i >= 0; --i) {
if (this.selectionArray[i].note === selection.note &&
this.selectionArray[i].startTime === selection.startTime &&
this.selectionArray[i].endTime === selection.endTime) {
this.selectionArray.splice(i, 1);
}
}
}
/**
* Draw this note group with the specified parameters from the containing pane and the specified note height.
* @param {d3.selection} parentElement - The parent element.
* @param {d3.scaleLinear} timeScale - The time scale.
* @param {number} noteHeight - The height of notes in pixels.
*/
draw(parentElement, timeScale, noteHeight) {
if (this.show) {
if (this.noteScale !== undefined) {
this.noteGroupGroup = parentElement.append("g")
.attr("class", "annotation noteGroupGroup")
.on("deselectNote", (event) => {
for (let i = this.selectionArray.length-1; i >= 0; --i) {
if (this.selectionArray[i].note === event.detail.note &&
this.selectionArray[i].startTime === event.detail.startTime &&
this.selectionArray[i].endTime === event.detail.endTime) {
this.selectionArray.splice(i, 1);
if (!this.firstCreated) {
this.status = AnnotationStatus.UPDATED;
this.dateUpdated = new Date().toISOString();
}
this.drawSelections(this.noteGroupGroup, timeScale, noteHeight, this.active);
}
}
});
this.drawSelections(this.noteGroupGroup, timeScale, noteHeight, this.active);
}
}
}
/**
* Draw note selections in this note group with the specified parameters.
* @param {d3.selection} parentElement - The parent element.
* @param {d3.scaleLinear} timeScale - The time scale.
* @param {number} noteHeight - The height of notes in pixels.
* @param {boolean} active - If true, note selections are active.
*/
drawSelections(parentElement, timeScale, noteHeight, active) {
this.eraseSelections();
if (this.selectionArray.length > 0) {
for (let i = 0; i < this.selectionArray.length; i++) {
this.selectionArray[i].draw(parentElement, timeScale, this.noteScale, noteHeight, active);
}
}
}
/**
* Erase the note in this note group.
*/
eraseSelections() {
this.noteGroupGroup.selectAll(".noteGroupSelectionRect").remove();
}
/**
* Erase this note group.
*/
erase() {
if (this.noteGroupGroup !== undefined) {
this.noteGroupGroup.remove();
}
}
/**
* Select or deselect this note group.
* @param {boolean} select - If true, select; otherwise deselect.
*/
select(select) {
this.selected = select;
// This check is for selecting note groups that share the same notes
if (this.selected) {
this.noteGroupGroup.raise();
}
else {
this.noteGroupGroup.lower();
}
for (let i = 0; i < this.selectionArray.length; i++) {
this.selectionArray[i].select(this.selected);
}
}
/**
* Set the label string.
* @param {string} label - The label.
*/
setLabel(label) {
this.label = label;
if (!this.firstCreated) {
this.status = AnnotationStatus.UPDATED;
this.dateUpdated = new Date().toISOString();
}
}
}
/** A note selection in a NoteGroup. */
class NoteGroupSelection {
/**
* Create a selected note for inclusion in a NoteGroup.
* @param {number} note - The time.
* @param {string} startTime - The start time of the note.
* @param {string} endTime - The end time of the note.
* @param {Object} options - Configuration options.
* @param {boolean} selected - Whether this note is currently selected.
* @author Lawrence Fyfe
*/
constructor(note, startTime, endTime, options, selected) {
this.note = note;
this.startTime = startTime;
this.endTime = endTime;
this.color = options?.color ?? "#800000";
this.selectedColor = options?.selectedColor ?? "#ff0000";
this.noteSelectionRect;
this.selected = selected;
}
/**
* Draw this note selection with the specified parameters from the containing note group.
* @param {d3.selection} parentElement - The parent element.
* @param {d3.scaleLinear} timeScale - The time scale.
* @param {d3.scaleLinear} noteScale - The scale of note values.
* @param {number} noteHeight - The height of notes in pixels.
* @param {boolean} active - True if this note selection should be active; false otherwise.
*/
draw(parentElement, timeScale, noteScale, noteHeight, active) {
let x1 = timeScale(this.startTime);
let x2 = timeScale(this.endTime);
let y = noteScale(this.note);
let w = x2-x1;
let h = noteHeight;
this.noteSelectionRect = parentElement.append("rect")
.attr("class", () => {
let selectionClass = "annotation noteGroupSelectionRect";
if (this.selected) {
return selectionClass + " selected";
}
else {
return selectionClass;
}
})
.attr("x", x1)
.attr("y", y)
.attr("width", w)
.attr("height", h)
.style("fill", "none")
.style("stroke", () => {
if (this.selected) {
return this.selectedColor;
}
else {
return this.color;
}
})
.style("stroke-width", 2)
.style("pointer-events", "visible")
.on("click", (event) => {
if (this.selected) {
event.target.dispatchEvent(new CustomEvent("deselectNote", {bubbles: true, detail: {note: this.note,
startTime: this.startTime,
endTime: this.endTime}}));
}
event.stopPropagation();
});
}
/**
* Select or deselect this note.
* @param {boolean} select - If true, select; otherwise deselect.
*/
select(select) {
this.selected = select;
if (this.selected) {
this.noteSelectionRect.style("stroke", this.selectedColor);
}
else {
this.noteSelectionRect.style("stroke", this.color);
}
}
}
/** A region annotation. */
class Region {
// Drage modes
static MOVE = 0;
static RESIZE_LEFT = 1;
static RESIZE_RIGHT = 2;
/**
* Create a region annotation.
* @param {number} startTime - The start time.
* @param {number} endTime - The end time.
* @param {string} label - A label.
* @param {Object} options - Configuration options.
* @param {Object} properties - Properties.
* @param {boolean} active - Whether this region should be active when created.
* @param {boolean} firstCreated - Whether this region has just been created.
* @author Lawrence Fyfe
*/
constructor(startTime, endTime, label, options, properties, active, firstCreated) {
this.type = "region";
this.status = AnnotationStatus.UNCHANGED;
this.selected = false;
this.startTime = startTime;
this.endTime = endTime;
this.label = label;
this.firstCreated = firstCreated;
this.dragMode = 0;
this.resizeWidthFactor = options?.resizeWidthFactor ?? 0.1;
this.color = options?.color ?? "#800000";
this.selectedColor = options?.selectedColor ?? "#ff0000";
this.alwaysShowLabel = options?.alwaysShowLabel ?? false;
this.properties = properties;
this.active = active;
this.show = true;
this.dateCreated;
this.dateUpdated;
this.regionGroup;
this.rect;
if (this.firstCreated) {
this.status = AnnotationStatus.CREATED;
this.dateCreated = new Date().toISOString();
}
}
/**
* Draw this region with the specified parameters from the containing pane.
* @param {d3.selection} parentElement - The parent element.
* @param {number} parentWidth - The width.
* @param {number} parentHeight - The height.
* @param {d3.scaleLinear} timeScale - The time scale.
*/
draw(parentElement, parentWidth, parentHeight, timeScale) {
if (this.show) {
var x = timeScale(this.startTime);
var width = timeScale(this.endTime)-x;
this.resizeHandleWidth = (this.resizeWidthFactor*width > 4) ? this.resizeWidthFactor*width : 4;
this.regionGroup = parentElement.append("g")
.attr("class", "annotation regionGroup");
this.rect = this.regionGroup.append("rect")
.attr("class", "regionRect")
.attr("x", x)
.attr("y", 0)
.attr("width", width)
.attr("height", parentHeight)
.style("fill", () => {
if (this.selected) {
return this.selectedColor;
}
else {
return this.color;
}
})
.style("fill-opacity", 0.20)
.style("stroke", this.color)
.style("stroke-width", 1)
.call(d3.drag()
.on("start", (event) => {
if (this.active) {
let mouseTime = timeScale.invert(event.x);
let resizeHandleTime = timeScale.invert(this.resizeHandleWidth) - timeScale.domain()[0];
if (mouseTime > this.startTime && mouseTime < (this.startTime + resizeHandleTime)) {
this.dragMode = Region.RESIZE_LEFT;
}
else if (mouseTime > (this.endTime - resizeHandleTime) && mouseTime < this.endTime) {
this.dragMode = Region.RESIZE_RIGHT;
}
else {
this.dragMode = Region.MOVE;
}
}
})
.on("drag", (event) => {
if (this.active && event.dx !== 0) {
let dt = timeScale.invert(event.x) - timeScale.invert(event.x-event.dx);
let minTime = timeScale.invert(12) - timeScale.domain()[0];
if (this.dragMode === Region.RESIZE_LEFT) {
this.rect.style("cursor", "w-resize");
if (this.startTime + dt < timeScale.domain()[0]) {
this.startTime = timeScale.domain()[0];
}
else {
if (this.startTime + dt < (this.endTime - minTime)) {
this.startTime += dt;
}
}
}
else if (this.dragMode === Region.RESIZE_RIGHT) {
this.rect.style("cursor", "e-resize");
if (this.endTime + dt > timeScale.domain()[1]) {
this.endTime = timeScale.domain()[1];
}
else {
if (this.endTime + dt > (this.startTime + minTime)) {
this.endTime += dt;
}
}
}
else if (this.dragMode === Region.MOVE) {
this.rect.style("cursor", "move");
if (this.startTime + dt > timeScale.domain()[0] && this.endTime + dt < timeScale.domain()[1]) {
this.startTime += dt;
this.endTime += dt;
}
}
let x = timeScale(this.startTime);
let width = timeScale(this.endTime) - x;
this.rect
.attr("x", x)
.attr("width", width);
this.drawLabel(parentWidth, parentHeight, x+(width/2));
}
})
.on("end", (event) => {
if (this.active) {
this.rect.style("cursor", "pointer");
if (this.dragMode === Region.RESIZE_LEFT || this.dragMode === Region.RESIZE_RIGHT) {
this.resizeHandleWidth = (this.resizeWidthFactor*(parseFloat(this.rect.attr("width"))) > 4) ?
this.resizeWidthFactor*(parseFloat(this.rect.attr("width"))) : 4;
}
if (!this.firstCreated) {
this.status = AnnotationStatus.UPDATED;
this.dateUpdated = new Date().toISOString();
}
event.sourceEvent.target.dispatchEvent(new CustomEvent("updateAnnotation", {bubbles: true, detail: {annotation: this}}));
}
}))
.on("mouseenter", (event) => {
if (this.active) {
this.changeCursor(timeScale.invert(d3.pointer(event)[0]), timeScale.invert(this.resizeHandleWidth) - timeScale.domain()[0]);
}
this.regionGroup.raise();
let x = timeScale(this.startTime);
let width = timeScale(this.endTime) - x;
this.drawLabel(parentWidth, parentHeight, x+(width/2));
})
.on("mousemove", (event) => {
if (this.active) {
this.changeCursor(timeScale.invert(d3.pointer(event)[0]), timeScale.invert(this.resizeHandleWidth) - timeScale.domain()[0]);
}
})
.on("mouseleave", (event) => {
this.eraseLabel();
})
.on("click", (event) => {
if (this.active) {
if (!this.selected) {
let soleSelection = true;
this.select(true);
if (event.metaKey || event.ctrlKey) {
soleSelection = false;
}
event.target.dispatchEvent(new CustomEvent("selectAnnotation", {bubbles: true, detail: {annotation: this, soleSelection: soleSelection}}));
}
else {
this.select(false);
event.target.dispatchEvent(new CustomEvent("deselectAnnotation", {bubbles: true}));
}
}
event.stopPropagation();
});
}
}
/**
* Erase this region.
*/
erase() {
if (this.regionGroup !== undefined) {
this.regionGroup.remove();
}
}
/**
* Change the cursor style based in the pointer location.
* @param {number} mouseTime - The time value in seconds at the pointer location.
* @param {number} resizeHandleTime - The size of the resize handles in seconds.
*/
changeCursor(mouseTime, resizeHandleTime) {
if (mouseTime > this.startTime && mouseTime < (this.startTime + resizeHandleTime)) {
this.rect.style("cursor", "w-resize");
}
else if (mouseTime > (this.endTime - resizeHandleTime) && mouseTime < this.endTime) {
this.rect.style("cursor", "e-resize");
}
else {
this.rect.style("cursor", "pointer");
}
}
/**
* Set the label string.
* @param {string} label - The label.
*/
setLabel(label) {
this.label = label;
if (!this.firstCreated) {
this.status = AnnotationStatus.UPDATED;
this.dateUpdated = new Date().toISOString();
}
}
/**
* Draw the label.
* @param {number} parentWidth - The width.
* @param {number} parentHeight - The height.
* @param {number} x - The x location.
*/
drawLabel(parentWidth, parentHeight, x) {
this.eraseLabel();
var labelGroup = this.regionGroup.append("g")
.attr("class", "regionLabel");
var labelText = labelGroup.append("text")
.attr("x", x)
.attr("y", parentHeight-16)
.style("font-family", "sans-serif")
.style("color", "black")
.style("text-anchor", "middle")
.style("dominant-baseline", "middle")
.text(this.label);
var textBox = labelText.node().getBBox();
var labelRect = labelGroup.append("rect")
.attr("x", textBox.x-2)
.attr("y", textBox.y-2)
.attr("width", textBox.width+4)
.attr("height", textBox.height+4)
.style("fill", "white");
labelText.raise();
}
/**
* Erase the label.
*/
eraseLabel() {
this.regionGroup.selectAll(".regionLabel").remove();
}
/**
* Select or deselect this region.
* @param {boolean} select - If true, select; otherwise deselect.
*/
select(select) {
this.selected = select;
if (this.selected) {
this.rect.style("fill", this.selectedColor);
}
else {
this.rect.style("fill", this.color);
}
}
}