279 lines
No EOL
7.5 KiB
JavaScript
279 lines
No EOL
7.5 KiB
JavaScript
class AnnotationManager {
|
|
constructor(rootEl, tagsUrl) {
|
|
this.rootEl = rootEl;
|
|
|
|
|
|
this.tagsEl = this.rootEl.querySelector('#tags');
|
|
this.annotationsEl = this.rootEl.querySelector('#annotations');
|
|
|
|
this.loadTags(tagsUrl);
|
|
}
|
|
|
|
loadTags(tagFile) {
|
|
// tags config
|
|
const request = new Request(tagFile);
|
|
return fetch(request)
|
|
.then(response => response.json())
|
|
.then(rootTag => {
|
|
this.rootTag = Tag.from(rootTag);
|
|
this.buildTagList()
|
|
});
|
|
}
|
|
|
|
buildTagList() {
|
|
// build, and rebuild
|
|
this.tagsEl.innerHTML = "";
|
|
|
|
const addTag = (tag, parentEl) => {
|
|
const tagLiEl = document.createElement('li');
|
|
const tagSubEl = document.createElement('ul');
|
|
const tagEl = document.createElement('div');
|
|
|
|
tagEl.innerText = tag.get_name();
|
|
tagEl.addEventListener('click', (ev) => {
|
|
this.selectTag(tag);
|
|
})
|
|
|
|
const colorEl = document.createElement('input');
|
|
colorEl.type = 'color';
|
|
colorEl.value = tag.get_color();
|
|
colorEl.addEventListener('change', (ev) => {
|
|
this.setTagColor(tag, ev.target.value);
|
|
});
|
|
tagEl.prepend(colorEl)
|
|
|
|
tagLiEl.classList.add('tag-id-' + tag.id);
|
|
tagLiEl.appendChild(tagEl);
|
|
|
|
tag.menuLiEl = tagLiEl;
|
|
|
|
tagLiEl.appendChild(tagSubEl);
|
|
tag.children.forEach((tag) => addTag(tag, tagSubEl));
|
|
const tagAddSubEl = document.createElement('li');
|
|
tagAddSubEl.classList.add('add-tag')
|
|
tagAddSubEl.innerText = 'add tag';
|
|
tagAddSubEl.addEventListener('click', (ev) => {
|
|
const name = prompt(`Add a tag under '${tag.get_name()}':`);
|
|
if (name === null || name.length < 1) return //cancel
|
|
this.addTag(name, tag);
|
|
});
|
|
tagSubEl.appendChild(tagAddSubEl);
|
|
|
|
|
|
parentEl.appendChild(tagLiEl);
|
|
|
|
};
|
|
|
|
addTag(this.rootTag, this.tagsEl);
|
|
}
|
|
|
|
|
|
selectTag(tag) {
|
|
this.clearSelectedTag();
|
|
|
|
tag.menuLiEl.classList.add('selected');
|
|
this.loadAnnotationsForTag(tag)
|
|
}
|
|
|
|
clearSelectedTag() {
|
|
const selected = this.tagsEl.querySelectorAll('.selected');
|
|
selected.forEach((s) => s.classList.remove('selected'));
|
|
//TODO empty annotationEl
|
|
this.annotationsEl.innerHTML = "";
|
|
}
|
|
|
|
selectAnnotation() {
|
|
throw new Error("Not implemented");
|
|
}
|
|
|
|
addTag(name, parentTag) {
|
|
const tag = new Tag();
|
|
tag.id = crypto.randomUUID();
|
|
tag.name = name;
|
|
tag.parent = parentTag;
|
|
parentTag.children.push(tag);
|
|
|
|
this.buildTagList();
|
|
this.saveTags();
|
|
}
|
|
|
|
loadAnnotationsForTag(tag) {
|
|
this.annotationsEl.innerHTML = "";
|
|
|
|
const request = new Request("/tags/" + tag.id);
|
|
return fetch(request)
|
|
.then(response => response.json())
|
|
.then(annotations => {
|
|
this.annotations = annotations;
|
|
this.buildAnnotationList()
|
|
});
|
|
}
|
|
|
|
buildAnnotationList() {
|
|
this.annotationsEl.innerHTML = "";
|
|
|
|
const ulEl = document.createElement('ul');
|
|
this.annotations.forEach((annotation, idx) => {
|
|
const liEl = document.createElement('li');
|
|
const imgEl = document.createElement('img');
|
|
const playerEl = document.createElement('div');
|
|
const infoEl = document.createElement('div');
|
|
infoEl.classList.add('annotation-info');
|
|
|
|
const tag = this.rootTag.find_by_id(annotation.tag);
|
|
console.log(tag)
|
|
|
|
infoEl.innerText = `[${tag.get_name()}] ${annotation.comment}`;
|
|
|
|
imgEl.src = `/annotation/${annotation.id}.svg`;
|
|
imgEl.addEventListener('click', () => {
|
|
imgEl.style.display = 'none';
|
|
new Annotator(
|
|
playerEl,
|
|
"tags.json",
|
|
annotation.url,
|
|
{ is_player: true, crop_to_fit: true, autoplay: true }
|
|
);
|
|
})
|
|
|
|
playerEl.classList.add('play');
|
|
|
|
liEl.appendChild(imgEl);
|
|
liEl.appendChild(playerEl);
|
|
liEl.appendChild(infoEl);
|
|
ulEl.appendChild(liEl);
|
|
|
|
});
|
|
|
|
this.annotationsEl.appendChild(ulEl);
|
|
|
|
}
|
|
|
|
renameTag(tag) {
|
|
// TODO: add button for this
|
|
const name = prompt(`Rename tag '${tag.get_name()}':`);
|
|
if (name === null || name.length < 1) return //cancel
|
|
tag.name = name;
|
|
|
|
this.saveTags();
|
|
}
|
|
|
|
removeTag(tag) {
|
|
// TODO: add button for this
|
|
const request = new Request("/tags/" + tag.id);
|
|
return fetch(request)
|
|
.then(response => response.json())
|
|
.then(annotations => {
|
|
if(annotations.length) {
|
|
alert(`Cannot remove '${tag.get_name()}', as it is used for ${annotations.length} annotations.`)
|
|
} else {
|
|
// TODO: remove tag
|
|
tag.parent.children.splice(tag.parent.children.indexOf(tag), 1);
|
|
tag.parent = null;
|
|
|
|
this.saveTags();
|
|
}
|
|
});
|
|
}
|
|
|
|
setTagColor(tag, color) {
|
|
tag.color = color;
|
|
this.buildTagList();
|
|
|
|
this.saveTags();
|
|
}
|
|
|
|
moveSelectedAnnotations(newTag) {
|
|
// TODO: add button for this
|
|
throw new Error("Not implemented");
|
|
}
|
|
|
|
saveTags() {
|
|
const json = this.rootTag.export();
|
|
// TODO: save to remote
|
|
console.log('save', json)
|
|
}
|
|
}
|
|
|
|
class Tag {
|
|
parent = null;
|
|
children = [];
|
|
|
|
static from(json_obj) {
|
|
if (json_obj.hasOwnProperty('children')) {
|
|
json_obj.children = json_obj.children.map(Tag.from)
|
|
} else {
|
|
json_obj.children = [];
|
|
}
|
|
json_obj.parent = null;
|
|
const tag = Object.assign(new Tag(), json_obj);
|
|
tag.children.map((child) => child.parent = tag);
|
|
return tag;
|
|
}
|
|
|
|
is_root() {
|
|
return this.parent === null;
|
|
}
|
|
|
|
descendants(inc_self = false) {
|
|
let tags = this.children.flatMap((t) => t.descendants(true))
|
|
if (inc_self)
|
|
tags.unshift(this)
|
|
return tags;
|
|
}
|
|
|
|
find_by_id(tag_id) {
|
|
const desc = this.descendants().filter((tag) => tag.id == tag_id);
|
|
if (desc.length) return desc[0];
|
|
return null;
|
|
}
|
|
|
|
depth() {
|
|
if (this.parent === null) {
|
|
return 0;
|
|
}
|
|
return this.parent.depth() + 1;
|
|
}
|
|
|
|
root() {
|
|
if (this.parent !== null) {
|
|
return this.parent.root();
|
|
}
|
|
return this;
|
|
}
|
|
|
|
get_name() {
|
|
if (this.hasOwnProperty('name')) {
|
|
return this.name;
|
|
}
|
|
return this.id;
|
|
}
|
|
|
|
get_color() {
|
|
if (this.hasOwnProperty('color')) {
|
|
return this.color;
|
|
}
|
|
if (this.parent !== null) {
|
|
return this.parent.get_color()
|
|
}
|
|
return 'black';
|
|
}
|
|
|
|
_json_replacer(key, value) {
|
|
if (key === 'children' && !value.length) {
|
|
return undefined;
|
|
}
|
|
if (key === 'parent' || key === 'menuLiEl') {
|
|
return undefined;
|
|
}
|
|
if (key === 'color' && this.parent !== null && this.parent.get_color() === value) {
|
|
return undefined;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
export() {
|
|
return JSON.stringify(this, this._json_replacer)
|
|
}
|
|
|
|
} |