Start with tag management. Incl. fancier index
This commit is contained in:
parent
232903c0ff
commit
ab9c66794c
6 changed files with 467 additions and 112 deletions
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
import asyncio
|
||||
import copy
|
||||
from ctypes.wintypes import tagMSG
|
||||
import json
|
||||
from os import X_OK, PathLike
|
||||
import os
|
||||
|
@ -12,7 +13,7 @@ import svgwrite
|
|||
import tempfile
|
||||
import io
|
||||
import logging
|
||||
from anytree import NodeMixin, RenderTree
|
||||
from anytree import NodeMixin, RenderTree, iterators
|
||||
from anytree.exporter import JsonExporter
|
||||
from anytree.importer import JsonImporter, DictImporter
|
||||
|
||||
|
@ -22,11 +23,12 @@ Milliseconds = float
|
|||
Seconds = float
|
||||
|
||||
class Annotation:
|
||||
def __init__(self, tag: str, drawing: Drawing, t_in: Milliseconds, t_out: Milliseconds) -> None:
|
||||
def __init__(self, tag: str, drawing: Drawing, t_in: Milliseconds, t_out: Milliseconds, comment: str = None) -> None:
|
||||
self.tag = tag
|
||||
self.t_in = t_in
|
||||
self.t_out = t_out
|
||||
self.drawing = drawing
|
||||
self.comment = comment
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
|
@ -502,6 +504,8 @@ class AnnotationIndex:
|
|||
self.drawing_dir = drawing_dir
|
||||
self.metadata_dir = metadata_dir
|
||||
|
||||
self.root_tag = getRootTag()
|
||||
|
||||
# disable disk cache because of glitches shelve.open(filename, writeback=True)
|
||||
self.shelve = {}
|
||||
|
||||
|
@ -518,7 +522,7 @@ class AnnotationIndex:
|
|||
Drawing(fn, self.metadata_dir, self.drawing_dir) for fn in self.get_drawing_filenames()
|
||||
]
|
||||
}
|
||||
self.shelve['_tags'] = {}
|
||||
self.shelve['_tags'] = {tag.id: [] for tag in self.root_tag.descendants}
|
||||
self.shelve['_annotations'] = {}
|
||||
|
||||
drawing: Drawing
|
||||
|
@ -528,7 +532,7 @@ class AnnotationIndex:
|
|||
continue
|
||||
for ann in meta['annotations']:
|
||||
annotation = Annotation(
|
||||
ann['tag'], drawing, ann['t_in'], ann['t_out'])
|
||||
ann['tag'], drawing, ann['t_in'], ann['t_out'], ann['comment'] if 'comment' in ann else "")
|
||||
self.shelve['_annotations'][annotation.id] = annotation
|
||||
if annotation.tag not in self.shelve['_tags']:
|
||||
self.shelve['_tags'][annotation.tag] = [annotation]
|
||||
|
@ -549,10 +553,13 @@ class AnnotationIndex:
|
|||
def annotations(self) -> dict[str, Annotation]:
|
||||
return self.shelve["_annotations"]
|
||||
|
||||
def get_annotations(self, tag) -> list[Annotation]:
|
||||
if tag not in self.tags:
|
||||
def has_tag(self, tag):
|
||||
return tag in self.tags
|
||||
|
||||
def get_annotations_for_tag(self, tag_id) -> list[Annotation]:
|
||||
if tag_id not in self.tags:
|
||||
return []
|
||||
return self.tags[tag]
|
||||
return self.tags[tag_id]
|
||||
|
||||
def get_drawing_names(self) -> list[str]:
|
||||
return [
|
||||
|
@ -567,6 +574,14 @@ class AnnotationIndex:
|
|||
for name in self.get_drawing_names()
|
||||
]
|
||||
|
||||
def get_nested_annotations_for_tag(self, tag_id) -> list[Annotation]:
|
||||
tag = self.root_tag.find_by_id(tag_id)
|
||||
annotations = []
|
||||
for tag in tag.descendants_incl_self():
|
||||
annotations.extend(self.get_annotations_for_tag(tag.id))
|
||||
return annotations
|
||||
|
||||
|
||||
def __del__(self):
|
||||
self.shelve.close()
|
||||
|
||||
|
@ -698,7 +713,7 @@ def strokes2D(strokes):
|
|||
return d
|
||||
|
||||
class Tag(NodeMixin):
|
||||
def __init__(self, id, name = None, description = "", color = "black", parent=None, children=None):
|
||||
def __init__(self, id, name = None, description = "", color = None, parent=None, children=None):
|
||||
self.id = id
|
||||
self.name = self.id if name is None else name
|
||||
self.color = color
|
||||
|
@ -707,20 +722,31 @@ class Tag(NodeMixin):
|
|||
if children:
|
||||
self.children = children
|
||||
|
||||
if self.id == 'root' and not self.is_root:
|
||||
logger.error("Root node shouldn't have a parent assigned")
|
||||
|
||||
class TagTree(NodeMixin):
|
||||
def __init__(self, parent=None, children=None):
|
||||
self.parent = parent
|
||||
if children:
|
||||
self.children = children
|
||||
def __repr__(self):
|
||||
return f"<svganim.strokes.Tag {self.id}>"
|
||||
|
||||
my0 = Tag('root')
|
||||
my1 = Tag('my1', 1, 0, parent=my0)
|
||||
my2 = Tag('my2', 0, 2, parent=my0)
|
||||
def get_color(self):
|
||||
if self.color is None and self.parent is not None:
|
||||
return self.parent.get_color()
|
||||
return self.color
|
||||
|
||||
print(RenderTree(my0))
|
||||
tree = JsonExporter(indent=2).export(my0)
|
||||
print(tree)
|
||||
print(RenderTree(JsonImporter(DictImporter(Tag)).import_(tree)))
|
||||
with open('www/tags.json', 'r') as fp:
|
||||
print(RenderTree(JsonImporter(DictImporter(Tag)).read(fp)))
|
||||
def descendants_incl_self(self):
|
||||
return tuple(iterators.PreOrderIter(self))
|
||||
|
||||
def find_by_id(self, tag_id) -> Optional[Tag]:
|
||||
for t in self.descendants:
|
||||
if t.id == tag_id:
|
||||
return t
|
||||
return None
|
||||
|
||||
def getRootTag(file = 'www/tags.json') -> Tag:
|
||||
with open(file, 'r') as fp:
|
||||
tree: Tag = JsonImporter(DictImporter(Tag)).read(fp)
|
||||
|
||||
# print(tree.descendants)
|
||||
return tree
|
||||
|
||||
# print(RenderTree(tree))
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from hashlib import md5
|
||||
|
||||
|
||||
def annotation_hash(handler, input):
|
||||
def annotation_hash(handler=None, input =""):
|
||||
return md5(input.encode()).hexdigest()
|
||||
|
||||
# def nmbr(handler, lst) -> int:
|
||||
|
|
|
@ -10,16 +10,64 @@
|
|||
color: white
|
||||
}
|
||||
|
||||
ul {
|
||||
#tags {
|
||||
font-size: 80%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#tags li {
|
||||
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
#tags .tag-id-root {
|
||||
list-style: none;
|
||||
;
|
||||
}
|
||||
|
||||
#tags .tag-id-root>ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
#tags li>div,
|
||||
#tags li.add-tag {
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
#tags .tag-id-root>div {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#tags li:hover>ul>li.add-tag {
|
||||
visibility: visible;
|
||||
;
|
||||
}
|
||||
|
||||
#tags .add-tag {
|
||||
visibility: hidden;
|
||||
;
|
||||
color: lightgray;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
#tags input[type="color"] {
|
||||
cursor: pointer;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
padding: 0;
|
||||
border: solid 1px black;
|
||||
border-radius: 2px;
|
||||
vertical-align: bottom;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#tags .selected>div {
|
||||
background: lightblue
|
||||
}
|
||||
|
||||
|
||||
summary h2 {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
|
@ -37,101 +85,87 @@
|
|||
display: block;;
|
||||
} */
|
||||
|
||||
img {
|
||||
|
||||
|
||||
#annotation_manager {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
grid-template-columns: 200px auto;
|
||||
}
|
||||
|
||||
#tags {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
#annotations {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
#annotations ul {
|
||||
grid-template-columns: repeat(auto-fill, 320px);
|
||||
grid-gap: 20px;
|
||||
display: grid;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
#annotations li {
|
||||
|
||||
}
|
||||
|
||||
#annotations img {
|
||||
/* width: 400px; */
|
||||
background: white;
|
||||
width: 300px;
|
||||
height: 200px;
|
||||
cursor: pointer;
|
||||
padding: 20px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.svganim_player {
|
||||
#annotations .svganim_player {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 300px;
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
padding: 20px;
|
||||
padding: 10px;
|
||||
|
||||
background: white;
|
||||
}
|
||||
|
||||
.svganim_player svg {
|
||||
#annotations .svganim_player svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.svganim_player.play:not(.loading) .controls {
|
||||
#annotations .svganim_player.play:not(.loading) .controls {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.svganim_player:hover .controls {
|
||||
#annotations .svganim_player:hover .controls {
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
<script src="assets/nouislider-15.5.0.js"></script>
|
||||
<script src="assets/wNumb-1.2.0.min.js"></script>
|
||||
<script src="annotate.js"></script>
|
||||
<script src="playlist.js"></script>
|
||||
<script src="annotations.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{% for tag in index.tags %}
|
||||
<details>
|
||||
<summary>
|
||||
<h2>{{tag}} ({{len(index.tags[tag])}})</h2>
|
||||
</summary>
|
||||
<ul>
|
||||
{% for annotation in index.tags[tag] %}
|
||||
<li>
|
||||
<img src="/annotation/{{ annotation.id }}.svg" loading="lazy" id="img-{{ annotation_hash(annotation.id) }}">
|
||||
<div class="play" id="annotation-{{ annotation_hash(annotation.id) }}"></div>
|
||||
<script type='text/javascript'>
|
||||
(function () {
|
||||
let imgEl = document.getElementById('img-{{ annotation_hash(annotation.id) }}');
|
||||
imgEl.addEventListener('click', () => {
|
||||
imgEl.style.display = 'none';
|
||||
new Annotator(
|
||||
document.getElementById("annotation-{{ annotation_hash(annotation.id) }}"),
|
||||
"tags.json",
|
||||
"{{ annotation.getJsonUrl() }}",
|
||||
{ is_player: true, crop_to_fit: true, autoplay: true }
|
||||
);
|
||||
})
|
||||
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
</li>
|
||||
<!-- <li><img src="/annotation/{{ annotation.id }}.svg" data-audio="/annotation/{{ annotation.id }}.mp3"></li> -->
|
||||
{% end %}
|
||||
</ul>
|
||||
</details>
|
||||
{% end %}
|
||||
<!-- <ul>
|
||||
{% for annotation in index.annotations %}
|
||||
<li>{{ annotation }}</li>
|
||||
{% end %}
|
||||
</ul> -->
|
||||
<div id="annotation_manager">
|
||||
<ul id="tags"></ul>
|
||||
<div id="annotations"></div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<a href="?refresh=1">Reload index</a>
|
||||
</body>
|
||||
<script>
|
||||
let images = document.querySelectorAll('[data-audio]');
|
||||
for (const image of images) {
|
||||
const audio = new Audio(image.dataset.audio);
|
||||
console.log(image, audio);
|
||||
image.addEventListener('mouseover', (e) => {
|
||||
audio.play();
|
||||
});
|
||||
image.addEventListener('mouseout', (e) => {
|
||||
audio.pause();
|
||||
audio.currentTime = 0;
|
||||
});
|
||||
}
|
||||
const am = new AnnotationManager(document.getElementById('annotation_manager'), 'tags.json');
|
||||
</script>
|
||||
|
||||
</html>
|
|
@ -345,12 +345,21 @@ class TagAnnotationsHandler(tornado.web.RequestHandler):
|
|||
self.metadir = os.path.join(self.config.storage, "metadata")
|
||||
|
||||
def get(self, tag):
|
||||
if tag not in self.index.tags:
|
||||
if not self.index.has_tag(tag):
|
||||
raise tornado.web.HTTPError(404)
|
||||
|
||||
self.set_header("Content-Type", "application/json")
|
||||
annotations = self.index.tags[tag]
|
||||
self.write(json.dumps(list([a.id for a in annotations])))
|
||||
# annotations = self.index.tags[tag]
|
||||
# self.write(json.dumps(list([a.id for a in annotations])))
|
||||
annotations = self.index.get_nested_annotations_for_tag(tag)
|
||||
self.write(json.dumps([{
|
||||
"id": annotation.id,
|
||||
"tag": annotation.tag,
|
||||
"id_hash": svganim.uimethods.annotation_hash(input=annotation.id),
|
||||
"url": annotation.getJsonUrl(),
|
||||
"comment": annotation.comment,
|
||||
"drawing": annotation.drawing.get_url()
|
||||
} for annotation in annotations]))
|
||||
|
||||
|
||||
class AnnotationHandler(tornado.web.RequestHandler):
|
||||
|
@ -504,6 +513,13 @@ class AnnotationsHandler(tornado.web.RequestHandler):
|
|||
with open(meta_file, "w") as fp:
|
||||
json.dump(self.json_args, fp)
|
||||
|
||||
class TagsHandler(tornado.web.RequestHandler):
|
||||
def initialize(self, config, index: svganim.strokes.AnnotationIndex) -> None:
|
||||
self.config = config
|
||||
self.index = index
|
||||
|
||||
def get(self):
|
||||
raise Exception('todo')
|
||||
|
||||
class IndexHandler(tornado.web.RequestHandler):
|
||||
"""Get annotation as svg"""
|
||||
|
|
|
@ -230,30 +230,30 @@ class Annotator extends EventTarget {
|
|||
this.tagsEl = document.createElement('ul');
|
||||
this.tagsEl.classList.add('tags');
|
||||
const addTags = (tags, tagsEl) => {
|
||||
Object.entries(tags).forEach(([tag, tagData]) => {
|
||||
tags.forEach((tag) => {
|
||||
let tagLiEl = document.createElement('li');
|
||||
let tagEl = document.createElement('div');
|
||||
|
||||
tagEl.classList.add('tag');
|
||||
tagEl.dataset.tag = tag;
|
||||
tagEl.innerText = tagData.hasOwnProperty('fullname') ? tagData.fullname : tag;
|
||||
tagEl.dataset.tag = tag.id;
|
||||
tagEl.innerText = tag.hasOwnProperty('name') ? tag.name : tag.id;
|
||||
tagEl.addEventListener('click', (e) => {
|
||||
this.addTag(tag, this.inPointPosition, this.outPointPosition);
|
||||
this.addTag(tag.id, this.inPointPosition, this.outPointPosition);
|
||||
});
|
||||
|
||||
tagEl.title = tagData.hasOwnProperty('description') ? tagData.description : "";
|
||||
tagEl.title = tag.hasOwnProperty('description') ? tag.description : "";
|
||||
|
||||
let signEl = document.createElement('span');
|
||||
signEl.classList.add('annotation-' + tag);
|
||||
signEl.style.backgroundColor = this.getColorForTag(tag);
|
||||
signEl.classList.add('annotation-' + tag.id);
|
||||
signEl.style.backgroundColor = this.getColorForTag(tag.id);
|
||||
tagEl.prepend(signEl);
|
||||
|
||||
tagLiEl.appendChild(tagEl);
|
||||
|
||||
if (tagData.hasOwnProperty('sub')) {
|
||||
if (tag.hasOwnProperty('children')) {
|
||||
const subEl = document.createElement('ul');
|
||||
subEl.classList.add('subtags');
|
||||
addTags(tagData.sub, subEl);
|
||||
addTags(tag.children, subEl);
|
||||
tagLiEl.appendChild(subEl);
|
||||
}
|
||||
|
||||
|
@ -304,14 +304,14 @@ class Annotator extends EventTarget {
|
|||
}
|
||||
}
|
||||
|
||||
getColorForTag(tag) {
|
||||
const tagData = this.tagMap[tag];
|
||||
console.log(tag, tagData);
|
||||
if (tagData && tagData.hasOwnProperty('color')) {
|
||||
return tagData.color;
|
||||
getColorForTag(tag_id) {
|
||||
const tag = this.tagMap[tag_id];
|
||||
console.log(tag_id, tag);
|
||||
if (tag && tag.hasOwnProperty('color')) {
|
||||
return tag.color;
|
||||
}
|
||||
if (tagData && tagData.hasOwnProperty('parent')) {
|
||||
return this.getColorForTag(tagData['parent']);
|
||||
if (tag && tag.hasOwnProperty('parent')) {
|
||||
return this.getColorForTag(tag['parent'].id);
|
||||
}
|
||||
return 'black';
|
||||
}
|
||||
|
@ -794,15 +794,15 @@ class Annotator extends EventTarget {
|
|||
const request = new Request(tagFile);
|
||||
return fetch(request)
|
||||
.then(response => response.json())
|
||||
.then(tags => {
|
||||
this.tags = tags;
|
||||
.then(rootTag => {
|
||||
this.tags = rootTag.children;
|
||||
this.tagMap = {};
|
||||
const addTagsToMap = (tags, parent) => {
|
||||
Object.entries(tags).forEach(([tag, tagData]) => {
|
||||
tagData['parent'] = typeof parent != "undefined" ? parent : null;
|
||||
this.tagMap[tag] = tagData;
|
||||
if (tagData.hasOwnProperty("sub")) {
|
||||
addTagsToMap(tagData.sub, tag);
|
||||
tags.forEach((tag) => {
|
||||
tag['parent'] = typeof parent != "undefined" ? parent : null;
|
||||
this.tagMap[tag.id] = tag;
|
||||
if (tag.hasOwnProperty("children")) {
|
||||
addTagsToMap(tag.children, tag);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
279
app/www/annotations.js
Normal file
279
app/www/annotations.js
Normal file
|
@ -0,0 +1,279 @@
|
|||
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)
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue