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
|
from __future__ import annotations
|
||||||
import asyncio
|
import asyncio
|
||||||
import copy
|
import copy
|
||||||
|
from ctypes.wintypes import tagMSG
|
||||||
import json
|
import json
|
||||||
from os import X_OK, PathLike
|
from os import X_OK, PathLike
|
||||||
import os
|
import os
|
||||||
|
@ -12,7 +13,7 @@ import svgwrite
|
||||||
import tempfile
|
import tempfile
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
from anytree import NodeMixin, RenderTree
|
from anytree import NodeMixin, RenderTree, iterators
|
||||||
from anytree.exporter import JsonExporter
|
from anytree.exporter import JsonExporter
|
||||||
from anytree.importer import JsonImporter, DictImporter
|
from anytree.importer import JsonImporter, DictImporter
|
||||||
|
|
||||||
|
@ -22,11 +23,12 @@ Milliseconds = float
|
||||||
Seconds = float
|
Seconds = float
|
||||||
|
|
||||||
class Annotation:
|
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.tag = tag
|
||||||
self.t_in = t_in
|
self.t_in = t_in
|
||||||
self.t_out = t_out
|
self.t_out = t_out
|
||||||
self.drawing = drawing
|
self.drawing = drawing
|
||||||
|
self.comment = comment
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def id(self) -> str:
|
def id(self) -> str:
|
||||||
|
@ -502,6 +504,8 @@ class AnnotationIndex:
|
||||||
self.drawing_dir = drawing_dir
|
self.drawing_dir = drawing_dir
|
||||||
self.metadata_dir = metadata_dir
|
self.metadata_dir = metadata_dir
|
||||||
|
|
||||||
|
self.root_tag = getRootTag()
|
||||||
|
|
||||||
# disable disk cache because of glitches shelve.open(filename, writeback=True)
|
# disable disk cache because of glitches shelve.open(filename, writeback=True)
|
||||||
self.shelve = {}
|
self.shelve = {}
|
||||||
|
|
||||||
|
@ -518,7 +522,7 @@ class AnnotationIndex:
|
||||||
Drawing(fn, self.metadata_dir, self.drawing_dir) for fn in self.get_drawing_filenames()
|
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'] = {}
|
self.shelve['_annotations'] = {}
|
||||||
|
|
||||||
drawing: Drawing
|
drawing: Drawing
|
||||||
|
@ -528,7 +532,7 @@ class AnnotationIndex:
|
||||||
continue
|
continue
|
||||||
for ann in meta['annotations']:
|
for ann in meta['annotations']:
|
||||||
annotation = Annotation(
|
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
|
self.shelve['_annotations'][annotation.id] = annotation
|
||||||
if annotation.tag not in self.shelve['_tags']:
|
if annotation.tag not in self.shelve['_tags']:
|
||||||
self.shelve['_tags'][annotation.tag] = [annotation]
|
self.shelve['_tags'][annotation.tag] = [annotation]
|
||||||
|
@ -549,10 +553,13 @@ class AnnotationIndex:
|
||||||
def annotations(self) -> dict[str, Annotation]:
|
def annotations(self) -> dict[str, Annotation]:
|
||||||
return self.shelve["_annotations"]
|
return self.shelve["_annotations"]
|
||||||
|
|
||||||
def get_annotations(self, tag) -> list[Annotation]:
|
def has_tag(self, tag):
|
||||||
if tag not in self.tags:
|
return tag in self.tags
|
||||||
|
|
||||||
|
def get_annotations_for_tag(self, tag_id) -> list[Annotation]:
|
||||||
|
if tag_id not in self.tags:
|
||||||
return []
|
return []
|
||||||
return self.tags[tag]
|
return self.tags[tag_id]
|
||||||
|
|
||||||
def get_drawing_names(self) -> list[str]:
|
def get_drawing_names(self) -> list[str]:
|
||||||
return [
|
return [
|
||||||
|
@ -567,6 +574,14 @@ class AnnotationIndex:
|
||||||
for name in self.get_drawing_names()
|
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):
|
def __del__(self):
|
||||||
self.shelve.close()
|
self.shelve.close()
|
||||||
|
|
||||||
|
@ -698,7 +713,7 @@ def strokes2D(strokes):
|
||||||
return d
|
return d
|
||||||
|
|
||||||
class Tag(NodeMixin):
|
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.id = id
|
||||||
self.name = self.id if name is None else name
|
self.name = self.id if name is None else name
|
||||||
self.color = color
|
self.color = color
|
||||||
|
@ -707,20 +722,31 @@ class Tag(NodeMixin):
|
||||||
if children:
|
if children:
|
||||||
self.children = 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 __repr__(self):
|
||||||
def __init__(self, parent=None, children=None):
|
return f"<svganim.strokes.Tag {self.id}>"
|
||||||
self.parent = parent
|
|
||||||
if children:
|
|
||||||
self.children = children
|
|
||||||
|
|
||||||
my0 = Tag('root')
|
def get_color(self):
|
||||||
my1 = Tag('my1', 1, 0, parent=my0)
|
if self.color is None and self.parent is not None:
|
||||||
my2 = Tag('my2', 0, 2, parent=my0)
|
return self.parent.get_color()
|
||||||
|
return self.color
|
||||||
|
|
||||||
print(RenderTree(my0))
|
def descendants_incl_self(self):
|
||||||
tree = JsonExporter(indent=2).export(my0)
|
return tuple(iterators.PreOrderIter(self))
|
||||||
print(tree)
|
|
||||||
print(RenderTree(JsonImporter(DictImporter(Tag)).import_(tree)))
|
def find_by_id(self, tag_id) -> Optional[Tag]:
|
||||||
with open('www/tags.json', 'r') as fp:
|
for t in self.descendants:
|
||||||
print(RenderTree(JsonImporter(DictImporter(Tag)).read(fp)))
|
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
|
from hashlib import md5
|
||||||
|
|
||||||
|
|
||||||
def annotation_hash(handler, input):
|
def annotation_hash(handler=None, input =""):
|
||||||
return md5(input.encode()).hexdigest()
|
return md5(input.encode()).hexdigest()
|
||||||
|
|
||||||
# def nmbr(handler, lst) -> int:
|
# def nmbr(handler, lst) -> int:
|
||||||
|
|
|
@ -10,16 +10,64 @@
|
||||||
color: white
|
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;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
#tags li>div,
|
||||||
display: inline-block;
|
#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 {
|
summary h2 {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -37,101 +85,87 @@
|
||||||
display: block;;
|
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; */
|
/* width: 400px; */
|
||||||
background: white;
|
background: white;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 20px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.svganim_player {
|
#annotations .svganim_player {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 20px;
|
padding: 10px;
|
||||||
|
|
||||||
background: white;
|
background: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.svganim_player svg {
|
#annotations .svganim_player svg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.svganim_player.play:not(.loading) .controls {
|
#annotations .svganim_player.play:not(.loading) .controls {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.svganim_player:hover .controls {
|
#annotations .svganim_player:hover .controls {
|
||||||
visibility: visible !important;
|
visibility: visible !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<script src="assets/nouislider-15.5.0.js"></script>
|
<script src="assets/nouislider-15.5.0.js"></script>
|
||||||
<script src="assets/wNumb-1.2.0.min.js"></script>
|
<script src="assets/wNumb-1.2.0.min.js"></script>
|
||||||
<script src="annotate.js"></script>
|
<script src="annotate.js"></script>
|
||||||
<script src="playlist.js"></script>
|
<script src="annotations.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{% for tag in index.tags %}
|
<div id="annotation_manager">
|
||||||
<details>
|
<ul id="tags"></ul>
|
||||||
<summary>
|
<div id="annotations"></div>
|
||||||
<h2>{{tag}} ({{len(index.tags[tag])}})</h2>
|
</div>
|
||||||
</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> -->
|
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<a href="?refresh=1">Reload index</a>
|
<a href="?refresh=1">Reload index</a>
|
||||||
</body>
|
</body>
|
||||||
<script>
|
<script>
|
||||||
let images = document.querySelectorAll('[data-audio]');
|
const am = new AnnotationManager(document.getElementById('annotation_manager'), 'tags.json');
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</html>
|
</html>
|
|
@ -345,12 +345,21 @@ class TagAnnotationsHandler(tornado.web.RequestHandler):
|
||||||
self.metadir = os.path.join(self.config.storage, "metadata")
|
self.metadir = os.path.join(self.config.storage, "metadata")
|
||||||
|
|
||||||
def get(self, tag):
|
def get(self, tag):
|
||||||
if tag not in self.index.tags:
|
if not self.index.has_tag(tag):
|
||||||
raise tornado.web.HTTPError(404)
|
raise tornado.web.HTTPError(404)
|
||||||
|
|
||||||
self.set_header("Content-Type", "application/json")
|
self.set_header("Content-Type", "application/json")
|
||||||
annotations = self.index.tags[tag]
|
# annotations = self.index.tags[tag]
|
||||||
self.write(json.dumps(list([a.id for a in annotations])))
|
# 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):
|
class AnnotationHandler(tornado.web.RequestHandler):
|
||||||
|
@ -504,6 +513,13 @@ class AnnotationsHandler(tornado.web.RequestHandler):
|
||||||
with open(meta_file, "w") as fp:
|
with open(meta_file, "w") as fp:
|
||||||
json.dump(self.json_args, 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):
|
class IndexHandler(tornado.web.RequestHandler):
|
||||||
"""Get annotation as svg"""
|
"""Get annotation as svg"""
|
||||||
|
|
|
@ -230,30 +230,30 @@ class Annotator extends EventTarget {
|
||||||
this.tagsEl = document.createElement('ul');
|
this.tagsEl = document.createElement('ul');
|
||||||
this.tagsEl.classList.add('tags');
|
this.tagsEl.classList.add('tags');
|
||||||
const addTags = (tags, tagsEl) => {
|
const addTags = (tags, tagsEl) => {
|
||||||
Object.entries(tags).forEach(([tag, tagData]) => {
|
tags.forEach((tag) => {
|
||||||
let tagLiEl = document.createElement('li');
|
let tagLiEl = document.createElement('li');
|
||||||
let tagEl = document.createElement('div');
|
let tagEl = document.createElement('div');
|
||||||
|
|
||||||
tagEl.classList.add('tag');
|
tagEl.classList.add('tag');
|
||||||
tagEl.dataset.tag = tag;
|
tagEl.dataset.tag = tag.id;
|
||||||
tagEl.innerText = tagData.hasOwnProperty('fullname') ? tagData.fullname : tag;
|
tagEl.innerText = tag.hasOwnProperty('name') ? tag.name : tag.id;
|
||||||
tagEl.addEventListener('click', (e) => {
|
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');
|
let signEl = document.createElement('span');
|
||||||
signEl.classList.add('annotation-' + tag);
|
signEl.classList.add('annotation-' + tag.id);
|
||||||
signEl.style.backgroundColor = this.getColorForTag(tag);
|
signEl.style.backgroundColor = this.getColorForTag(tag.id);
|
||||||
tagEl.prepend(signEl);
|
tagEl.prepend(signEl);
|
||||||
|
|
||||||
tagLiEl.appendChild(tagEl);
|
tagLiEl.appendChild(tagEl);
|
||||||
|
|
||||||
if (tagData.hasOwnProperty('sub')) {
|
if (tag.hasOwnProperty('children')) {
|
||||||
const subEl = document.createElement('ul');
|
const subEl = document.createElement('ul');
|
||||||
subEl.classList.add('subtags');
|
subEl.classList.add('subtags');
|
||||||
addTags(tagData.sub, subEl);
|
addTags(tag.children, subEl);
|
||||||
tagLiEl.appendChild(subEl);
|
tagLiEl.appendChild(subEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -304,14 +304,14 @@ class Annotator extends EventTarget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getColorForTag(tag) {
|
getColorForTag(tag_id) {
|
||||||
const tagData = this.tagMap[tag];
|
const tag = this.tagMap[tag_id];
|
||||||
console.log(tag, tagData);
|
console.log(tag_id, tag);
|
||||||
if (tagData && tagData.hasOwnProperty('color')) {
|
if (tag && tag.hasOwnProperty('color')) {
|
||||||
return tagData.color;
|
return tag.color;
|
||||||
}
|
}
|
||||||
if (tagData && tagData.hasOwnProperty('parent')) {
|
if (tag && tag.hasOwnProperty('parent')) {
|
||||||
return this.getColorForTag(tagData['parent']);
|
return this.getColorForTag(tag['parent'].id);
|
||||||
}
|
}
|
||||||
return 'black';
|
return 'black';
|
||||||
}
|
}
|
||||||
|
@ -794,15 +794,15 @@ class Annotator extends EventTarget {
|
||||||
const request = new Request(tagFile);
|
const request = new Request(tagFile);
|
||||||
return fetch(request)
|
return fetch(request)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(tags => {
|
.then(rootTag => {
|
||||||
this.tags = tags;
|
this.tags = rootTag.children;
|
||||||
this.tagMap = {};
|
this.tagMap = {};
|
||||||
const addTagsToMap = (tags, parent) => {
|
const addTagsToMap = (tags, parent) => {
|
||||||
Object.entries(tags).forEach(([tag, tagData]) => {
|
tags.forEach((tag) => {
|
||||||
tagData['parent'] = typeof parent != "undefined" ? parent : null;
|
tag['parent'] = typeof parent != "undefined" ? parent : null;
|
||||||
this.tagMap[tag] = tagData;
|
this.tagMap[tag.id] = tag;
|
||||||
if (tagData.hasOwnProperty("sub")) {
|
if (tag.hasOwnProperty("children")) {
|
||||||
addTagsToMap(tagData.sub, tag);
|
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