Compare commits

..

No commits in common. "adce8d067c46bd1dd15da116fb64fe62dd745179" and "9c54b2f8d7ab84b3ad6a42184a6febb03b953b14" have entirely different histories.

9 changed files with 165 additions and 969 deletions

View File

@ -1,7 +1,6 @@
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
@ -13,9 +12,6 @@ import svgwrite
import tempfile import tempfile
import io import io
import logging import logging
from anytree import NodeMixin, RenderTree, iterators
from anytree.exporter import JsonExporter, DictExporter
from anytree.importer import JsonImporter, DictImporter
logger = logging.getLogger('svganim.strokes') logger = logging.getLogger('svganim.strokes')
@ -23,12 +19,11 @@ Milliseconds = float
Seconds = float Seconds = float
class Annotation: class Annotation:
def __init__(self, tag: str, drawing: Drawing, t_in: Milliseconds, t_out: Milliseconds, comment: str = None) -> None: def __init__(self, tag: str, drawing: Drawing, t_in: Milliseconds, t_out: Milliseconds) -> 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:
@ -504,8 +499,6 @@ 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 = {}
@ -522,9 +515,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.root_tag = getRootTag()
self.shelve['_tags'] = {tag.id: [] for tag in self.root_tag.descendants}
self.shelve['_annotations'] = {} self.shelve['_annotations'] = {}
drawing: Drawing drawing: Drawing
@ -534,19 +525,14 @@ 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['comment'] if 'comment' in ann else "") ann['tag'], drawing, ann['t_in'], ann['t_out'])
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]
logger.error(f"Use of non-existing tag {annotation.tag}")
else: else:
self.shelve['_tags'][annotation.tag].append( self.shelve['_tags'][annotation.tag].append(
annotation annotation
) )
tag = self.root_tag.find_by_id(annotation.tag)
if tag is not None:
tag.annotation_count += 1
@property @property
def drawings(self) -> dict[str, Drawing]: def drawings(self) -> dict[str, Drawing]:
@ -560,13 +546,10 @@ class AnnotationIndex:
def annotations(self) -> dict[str, Annotation]: def annotations(self) -> dict[str, Annotation]:
return self.shelve["_annotations"] return self.shelve["_annotations"]
def has_tag(self, tag): def get_annotations(self, tag) -> list[Annotation]:
return tag in self.tags if tag not 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_id] return self.tags[tag]
def get_drawing_names(self) -> list[str]: def get_drawing_names(self) -> list[str]:
return [ return [
@ -581,14 +564,6 @@ 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()
@ -718,56 +693,3 @@ def strokes2D(strokes):
d += f"{rel_stroke[0]},{rel_stroke[1]} " d += f"{rel_stroke[0]},{rel_stroke[1]} "
last_stroke = stroke last_stroke = stroke
return d return d
class Tag(NodeMixin):
def __init__(self, id, name = None, description = "", color = None, parent=None, children=None, annotation_count=None):
self.id = id
self.name = self.id if name is None else name
self.color = color
self.description = description
self.parent = parent
if children:
self.children = children
self.annotation_count = 0 #always zero!
if self.id == 'root' and not self.is_root:
logger.error("Root node shouldn't have a parent assigned")
def __repr__(self):
return f"<svganim.strokes.Tag {self.id}>"
def __str__(self):
return RenderTree(self).by_attr('name')
def get_color(self):
if self.color is None and self.parent is not None:
return self.parent.get_color()
return self.color
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 toJson(self, with_counts=False) -> str:
ignore_counts=lambda attrs: [(k, v) for k, v in attrs if k != "annotation_count"]
attrFilter = None if with_counts else ignore_counts
return JsonExporter(DictExporter(attriter=attrFilter), indent=2).export(self)
def loadTagFromJson(string) -> Tag:
tree: Tag = JsonImporter(DictImporter(Tag)).import_(string)
return tree
def getRootTag(file = 'www/tags.json') -> Tag:
with open(file, 'r') as fp:
tree: Tag = JsonImporter(DictImporter(Tag)).read(fp)
return tree
# print(RenderTree(tree))

View File

@ -1,7 +1,7 @@
from hashlib import md5 from hashlib import md5
def annotation_hash(handler=None, input =""): def annotation_hash(handler, input):
return md5(input.encode()).hexdigest() return md5(input.encode()).hexdigest()
# def nmbr(handler, lst) -> int: # def nmbr(handler, lst) -> int:

View File

@ -10,106 +10,16 @@
color: white color: white
} }
#tags { ul {
font-size: 80%;
padding: 0;
margin: 0;
}
#tags li {
line-height: 1.5;
list-style: none;
}
#tags .tag-id-root {
list-style: none;
;
}
#tags .tag-id-root>ul {
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
#tags li>div, li {
#tags li.add-tag {
cursor: pointer;
}
#tags .tag-id-root>div {
display: none;
}
/* #tags li:hover>ul>li.add-tag {
visibility: visible;
;
} */
#tags li.selected>ul>li.add-tag {
display: block;
}
#tags>li>ul>li.add-tag {
display: block;
;
}
#annotations .annotations-actions {
margin-bottom: 10px;
color: darkgray;
font-size: 80%;
border-bottom: solid 3px #444;
}
#tags .add-tag {
display: none;
;
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: middle;
margin-right: 10px;
}
#tags li:hover>div>input.rm-tag {
display: inline-block; display: inline-block;
;
} }
#tags li div {
position: relative
}
#tags input.rm-tag:hover {
color: red;
transform: rotate(20deg);
}
#tags input.rm-tag {
/* display: none; */
position: absolute;
right: 0;
top: 0;
padding: 0;
background: none;
border: none;
color: white;
cursor: pointer;
}
#tags .selected>div {
background: lightblue
}
summary h2{ summary h2{
display: inline-block; display: inline-block;
cursor: pointer; cursor: pointer;
@ -127,84 +37,101 @@
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: 10px; padding: 20px;
} }
#annotations .svganim_player { .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: 10px; padding: 20px;
background: white; background: white;
} }
#annotations .svganim_player svg { .svganim_player svg {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
#annotations .svganim_player.play:not(.loading) .controls { .svganim_player.play:not(.loading) .controls {
visibility: hidden; visibility: hidden;
} }
#annotations .svganim_player:hover .controls { .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="annotations.js"></script> <script src="playlist.js"></script>
</head> </head>
<body> <body>
<div id="annotation_manager"> {% for tag in index.tags %}
<ul id="tags"></ul> <details>
<div id="annotations"></div> <summary>
</div> <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> -->
<hr> <hr>
<a href="?refresh=1">Reload index</a> <a href="?refresh=1">Reload index</a>
</body> </body>
<script> <script>
const am = new AnnotationManager(document.getElementById('annotation_manager'), 'tags.json'); 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;
});
}
</script> </script>
</html> </html>

View File

@ -1,8 +1,6 @@
import json import json
import logging import logging
import os import os
import shutil
from urllib.error import HTTPError
import tornado.ioloop import tornado.ioloop
import tornado.web import tornado.web
import tornado.websocket import tornado.websocket
@ -13,7 +11,6 @@ import html
import argparse import argparse
import coloredlogs import coloredlogs
import glob import glob
import filelock
import svganim.strokes import svganim.strokes
import svganim.uimethods import svganim.uimethods
@ -348,21 +345,12 @@ 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 not self.index.has_tag(tag): if tag not in self.index.tags:
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):
@ -413,41 +401,6 @@ class AnnotationHandler(tornado.web.RequestHandler):
"audio": f"/annotation/{annotation.id}.mp3", "audio": f"/annotation/{annotation.id}.mp3",
})) }))
def post(self, annotation_id):
"""change tag for given annotation"""
if annotation_id not in self.index.annotations:
raise tornado.web.HTTPError(404)
# might be set on file level, but let's try to avoid issues by keeping it simple
lock = filelock.FileLock("metadata_write.lock", timeout=10)
with lock:
newTagId = self.get_argument('tag_id')
if not self.index.has_tag(newTagId):
raise tornado.web.HTTPError(400)
annotation: svganim.strokes.Annotation = self.index.annotations[annotation_id]
logger.info(f"change tag from {annotation.tag} to {newTagId}")
# change metadata and reload index
metadata = annotation.drawing.get_metadata()
change = False
for idx, ann in enumerate(metadata['annotations']):
if ann['t_in'] == annotation.t_in and ann['t_out'] == annotation.t_out and annotation.tag == ann['tag']:
#found!?
metadata['annotations'][idx]['tag'] = newTagId
change = True
break
if change == False:
raise HTTPError(409)
with open(annotation.drawing.metadata_fn, "w") as fp:
logger.info(f"save tag in {annotation.drawing.metadata_fn}")
json.dump(metadata, fp)
self.index.refresh()
class DrawingHandler(tornado.web.RequestHandler): class DrawingHandler(tornado.web.RequestHandler):
"""Get drawing as svg""" """Get drawing as svg"""
@ -551,44 +504,6 @@ 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):
self.set_header("Content-Type", "application/json")
self.write(self.index.root_tag.toJson(with_counts=True))
# with open('www/tags.json', 'r') as fp:
# # TODO: enrich with counts
# self.write(fp.read())
def put(self):
# data = json.loads(self.request.body)
tree = svganim.strokes.loadTagFromJson(self.request.body)
logger.info(f"New tag tree:\n{tree}")
newTagsContent = tree.toJson()
# save at minute resolution
now = datetime.datetime.utcnow().isoformat(timespec='minutes')
backup_dir = os.path.join(self.config.storage, 'tag_versions')
if not os.path.exists(backup_dir):
logger.warning(f"Creating tags backupdir {backup_dir}")
os.mkdir(backup_dir)
bakfile = os.path.join(backup_dir, f'tags.{now}.json')
logger.info(f"Creating tags backup {bakfile}" )
shutil.copyfile('www/tags.json', bakfile)
with open('www/tags.json', 'w') as fp:
fp.write(newTagsContent)
# update as to load new tag into cache
self.index.refresh()
self.set_status(204)
# print()
class IndexHandler(tornado.web.RequestHandler): class IndexHandler(tornado.web.RequestHandler):
"""Get annotation as svg""" """Get annotation as svg"""
@ -672,9 +587,6 @@ class Server:
(r"/index", IndexHandler, (r"/index", IndexHandler,
{"config": self.config, "index": self.index}), {"config": self.config, "index": self.index}),
(r"/tags.json", TagsHandler,
{"config": self.config, "index": self.index}),
(r"/(.*)", StaticFileWithHeaderHandler, (r"/(.*)", StaticFileWithHeaderHandler,
{"path": self.web_root, 'default_filename': 'index.html'}), {"path": self.web_root, 'default_filename': 'index.html'}),
], ],

View File

@ -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) => {
tags.forEach((tag) => { Object.entries(tags).forEach(([tag, tagData]) => {
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.id; tagEl.dataset.tag = tag;
tagEl.innerText = tag.hasOwnProperty('name') ? tag.name : tag.id; tagEl.innerText = tagData.hasOwnProperty('fullname') ? tagData.fullname : tag;
tagEl.addEventListener('click', (e) => { tagEl.addEventListener('click', (e) => {
this.addTag(tag.id, this.inPointPosition, this.outPointPosition); this.addTag(tag, this.inPointPosition, this.outPointPosition);
}); });
tagEl.title = tag.hasOwnProperty('description') ? tag.description : ""; tagEl.title = tagData.hasOwnProperty('description') ? tagData.description : "";
let signEl = document.createElement('span'); let signEl = document.createElement('span');
signEl.classList.add('annotation-' + tag.id); signEl.classList.add('annotation-' + tag);
signEl.style.backgroundColor = this.getColorForTag(tag.id); signEl.style.backgroundColor = this.getColorForTag(tag);
tagEl.prepend(signEl); tagEl.prepend(signEl);
tagLiEl.appendChild(tagEl); tagLiEl.appendChild(tagEl);
if (tag.hasOwnProperty('children')) { if (tagData.hasOwnProperty('sub')) {
const subEl = document.createElement('ul'); const subEl = document.createElement('ul');
subEl.classList.add('subtags'); subEl.classList.add('subtags');
addTags(tag.children, subEl); addTags(tagData.sub, subEl);
tagLiEl.appendChild(subEl); tagLiEl.appendChild(subEl);
} }
@ -304,14 +304,14 @@ class Annotator extends EventTarget {
} }
} }
getColorForTag(tag_id) { getColorForTag(tag) {
const tag = this.tagMap[tag_id]; const tagData = this.tagMap[tag];
console.log(tag_id, tag); console.log(tag, tagData);
if (tag && tag.hasOwnProperty('color')) { if (tagData && tagData.hasOwnProperty('color')) {
return tag.color; return tagData.color;
} }
if (tag && tag.hasOwnProperty('parent')) { if (tagData && tagData.hasOwnProperty('parent')) {
return this.getColorForTag(tag['parent'].id); return this.getColorForTag(tagData['parent']);
} }
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(rootTag => { .then(tags => {
this.tags = rootTag.children; this.tags = tags;
this.tagMap = {}; this.tagMap = {};
const addTagsToMap = (tags, parent) => { const addTagsToMap = (tags, parent) => {
tags.forEach((tag) => { Object.entries(tags).forEach(([tag, tagData]) => {
tag['parent'] = typeof parent != "undefined" ? parent : null; tagData['parent'] = typeof parent != "undefined" ? parent : null;
this.tagMap[tag.id] = tag; this.tagMap[tag] = tagData;
if (tag.hasOwnProperty("children")) { if (tagData.hasOwnProperty("sub")) {
addTagsToMap(tag.children, tag); addTagsToMap(tagData.sub, tag);
} }
}); });
}; };

View File

@ -1,425 +0,0 @@
class AnnotationManager {
constructor(rootEl, tagsUrl) {
this.rootEl = rootEl;
this.tagsEl = this.rootEl.querySelector('#tags');
this.annotationsEl = this.rootEl.querySelector('#annotations');
this.selectedAnnotations = [];
this.selectedTag = null;
this.tagsUrl = tagsUrl;
this.loadTags();
}
loadTags() {
// tags config
const request = new Request(this.tagsUrl);
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()} (${tag.annotation_count ?? 0})`;
tagEl.addEventListener('click', (ev) => {
this.selectTag(tag);
})
tagEl.addEventListener('dblclick', (ev) => {
this.renameTag(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)
const rmEl = document.createElement('input');
rmEl.type = 'button';
rmEl.classList.add('rm-tag');
rmEl.value = '🗑';
rmEl.addEventListener('click', (ev) => {
ev.stopPropagation();
this.removeTag(tag);
});
tagEl.appendChild(rmEl)
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();
this.selectedTag = tag;
tag.menuLiEl.classList.add('selected');
this.loadAnnotationsForTag(tag)
}
clearSelectedTag() {
this.selectedTag = null;
const selected = this.tagsEl.querySelectorAll('.selected');
selected.forEach((s) => s.classList.remove('selected'));
this.resetSelectedAnnotations()
//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 = "";
this.actionsEl = document.createElement('div');
this.actionsEl.classList.add('annotations-actions')
this.buildAnnotationActions();
this.annotationsEl.appendChild(this.actionsEl);
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('span');
infoEl.classList.add('annotation-info');
const selectEl = document.createElement('input');
selectEl.type = 'checkbox';
selectEl.id = 'select-'+annotation.id_hash;
selectEl.addEventListener('change', (ev) => {
if (ev.target.checked) {
this.addAnnotationToSelection(annotation);
} else {
this.removeAnnotationFromSelection(annotation);
}
})
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(selectEl);
liEl.appendChild(infoEl);
ulEl.appendChild(liEl);
});
this.annotationsEl.appendChild(ulEl);
}
resetSelectedAnnotations() {
this.selectedAnnotations = [];
this.annotationsEl.querySelectorAll("li input[type='checkbox']").forEach((box) => box.checked = false);
this.buildAnnotationActions()
}
addAnnotationToSelection(annotation) {
if (this.selectedAnnotations.indexOf(annotation) === -1){
this.selectedAnnotations.push(annotation);
}
this.annotationsEl.querySelector('#select-'+annotation.id_hash).checked = true;
this.buildAnnotationActions()
}
removeAnnotationFromSelection(annotation) {
if (this.selectedAnnotations.indexOf(annotation) !== -1){
this.selectedAnnotations.splice(this.selectedAnnotations.indexOf(annotation), 1)
}
this.annotationsEl.querySelector('#select-'+annotation.id_hash).checked = true;
this.buildAnnotationActions()
}
buildAnnotationActions() {
if(!this.actionsEl || !this.annotations.length) return
this.actionsEl.innerHTML = "";
const selectAllLabelEl = document.createElement('label');
selectAllLabelEl.innerText = `Select all`
const selectAllEl = document.createElement('input');
selectAllEl.type = 'checkbox';
selectAllEl.checked = this.annotations.length === this.selectedAnnotations.length;
// selectAllEl.innerText = `Select all ${this.annotations.length} items`;
selectAllEl.addEventListener('change', (ev) => {
if(ev.target.checked) {
this.annotations.forEach((a) => this.addAnnotationToSelection(a));
} else {
this.resetSelectedAnnotations();
}
});
selectAllLabelEl.appendChild(selectAllEl)
this.actionsEl.appendChild(selectAllLabelEl)
if(!this.selectedAnnotations.length) return;
const moveLabelEl = document.createElement('label');
moveLabelEl.innerText = `Change tag for ${this.selectedAnnotations.length} items`
const moveSelectEl = document.createElement('select');
this.rootTag.descendants().forEach((tag, i) => {
const tagEl = document.createElement('option');
tagEl.value = tag.id;
if(tag.id == this.selectedTag.id){
tagEl.selected = true;
}
tagEl.innerHTML = tag.get_indented_name();
moveSelectEl.appendChild(tagEl);
});
moveSelectEl.addEventListener('change', (ev) => {
const tag = this.rootTag.find_by_id(ev.target.value);
console.log(tag);
this.moveSelectedAnnotations(tag);
})
moveLabelEl.appendChild(moveSelectEl)
this.actionsEl.appendChild(moveLabelEl)
}
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();
this.buildTagList();
}
removeTag(tag) {
if (!confirm(`Do you want to delete ${tag.get_name()}`)) {
return false;
}
// TODO: add button for this
const request = new Request("/tags/" + tag.id);
return fetch(request)
.then(response => {
if (response.status == 404) {
return [] // not existing tag surely has no annotations
} else {
return 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();
this.buildTagList();
}
})
}
setTagColor(tag, color) {
tag.color = color;
this.buildTagList();
this.saveTags();
}
async moveSelectedAnnotations(tag) {
// TODO: add button for this
// alert(`This doesn't work yet! (move to tag ${tag.get_name()})`)
await Promise.all(this.selectedAnnotations.map(async (annotation) => {
const formData = new FormData();
formData.append('tag_id', tag.id)
return await fetch('/annotation/'+annotation.id, {
method: 'POST',
// headers: {
// 'Content-Type': 'application/json'
// },
body: formData //JSON.stringify({'tag_id': tag.id})
}).catch((e) => alert('Something went wrong saving the tags'));
}));
this.loadAnnotationsForTag(this.selectedTag)
this.loadTags() //updates the counts
}
async saveTags() {
const json = this.rootTag.export();
console.log('save', json)
const response = await fetch('/tags.json', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: json
}).catch((e) => alert('Something went wrong saving the tags'));
}
}
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') && this.name !== null) {
return this.name;
}
return this.id;
}
get_indented_name() {
const name = this.get_name();
return '&nbsp;'.repeat((this.depth() - 1) * 2) + '- ' + name
}
get_color() {
if (this.hasOwnProperty('color') && this.color !== null) {
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)
}
}

View File

@ -1,172 +1,81 @@
{ {
"id": "root", "human-machine": {
"name": "root", "fullname": "Human/machine Entanglements (Appearance/disappearance)",
"color": null, "color": "orange",
"description": "", "sub": {
"children": [ "vision": {
{ "fullname": "Vision",
"id": "human-machine", "color": "orange"
"name": "Human/machine Entanglements (Appearance/disappearance)",
"color": "#ffa348",
"description": "",
"children": [
{
"id": "vision",
"name": "Vision",
"color": null,
"description": ""
}, },
{ "sound": {},
"id": "sound", "behaviour": {},
"name": "sound", "other-senses": {
"color": null, "fullname": "Other senses"
"description": "" }
},
{
"id": "behaviour",
"name": "behaviour",
"color": null,
"description": ""
},
{
"id": "other-senses",
"name": "Other senses",
"color": null,
"description": ""
} }
]
}, },
{ "tensions": {
"id": "tensions", "fullname": "Tensions, contestations & problems",
"name": "Tensions, contestations & problems", "description" : "Which problems are identified?, when do they become problems?",
"color": "#77767b", "color": "gray"
"description": "Which problems are identified?, when do they become problems?"
}, },
{ "security": {
"id": "security", "color": "blue",
"name": "Security & types of data", "fullname": "Security & types of data",
"color": "#3584e4", "sub": {
"description": "", "definitions": {
"children": [
{
"id": "definitions",
"name": "definitions",
"color": null,
"description": "e.g. domain knowledge" "description": "e.g. domain knowledge"
}, },
{ "input": {
"id": "input",
"name": "input",
"color": null,
"description": "e.g. fake data" "description": "e.g. fake data"
} }
] }
}, },
{ "actants":{
"id": "actants", "fullname": "Actants in relation",
"name": "Actants in relation", "color": "pink",
"color": "#fa08ff", "sub":{
"description": "", "algorithm": {},
"children": [ "technologies": {},
{ "frt": {},
"id": "algorithm", "cameras": {
"name": "algorithm", "fullname": "CCTV & camera's"
"color": null,
"description": ""
}, },
{ "entities":{
"id": "technologies", "fullname": "Entities: people, institutions etc."
"name": "technologies",
"color": null,
"description": ""
}, },
{ "positioning":{
"id": "frt", "fullname": "Positioning",
"name": "frt",
"color": null,
"description": ""
},
{
"id": "cameras",
"name": "CCTV & camera's",
"color": null,
"description": ""
},
{
"id": "entities",
"name": "Entities: people, institutions etc.",
"color": null,
"description": ""
},
{
"id": "positioning",
"name": "Positioning",
"color": null,
"description": "the positioning of a field/person/oneself in relation to others" "description": "the positioning of a field/person/oneself in relation to others"
}, },
{ "inside-outside":{},
"id": "inside-outside", "public-private":{}
"name": "inside-outside",
"color": null,
"description": ""
},
{
"id": "public-private",
"name": "public-private",
"color": null,
"description": ""
} }
]
}, },
{ "consequences":{
"id": "consequences", "color": "green",
"name": "consequences", "sub":{
"color": "#0add32", "effects":{},
"description": "", "future-imaginaries":{},
"children": [ "speculations": {
{
"id": "effects",
"name": "effects",
"color": null,
"description": ""
},
{
"id": "future-imaginaries",
"name": "future-imaginaries",
"color": null,
"description": ""
},
{
"id": "speculations",
"name": "speculations",
"color": null,
"description": "what is & what will/can be done." "description": "what is & what will/can be done."
}, },
{ "innovations": {}
"id": "innovations",
"name": "innovations",
"color": null,
"description": ""
} }
]
}, },
{
"id": "hesitation", "hesitation":{
"name": "Hesitations & corrections", "fullname": "Hesitations & corrections",
"color": "#f8e45c", "color": "yellow"
"description": ""
}, },
{
"id": "skip", "skip":{
"name": "skip", "color": "black"
"color": null,
"description": ""
}, },
{
"id": "todo", "todo":{
"name": "to do / interesting", "fullname": "to do / interesting",
"color": "#ff0000", "color": "red"
"description": ""
} }
]
} }

49
poetry.lock generated
View File

@ -1,18 +1,3 @@
[[package]]
name = "anytree"
version = "2.8.0"
description = "Powerful and Lightweight Python Tree Data Structure.."
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
six = ">=1.9.0"
[package.extras]
dev = ["check-manifest"]
test = ["coverage"]
[[package]] [[package]]
name = "coloredlogs" name = "coloredlogs"
version = "15.0.1" version = "15.0.1"
@ -27,18 +12,6 @@ humanfriendly = ">=9.1"
[package.extras] [package.extras]
cron = ["capturer (>=2.4)"] cron = ["capturer (>=2.4)"]
[[package]]
name = "filelock"
version = "3.7.1"
description = "A platform independent file lock."
category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"]
testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"]
[[package]] [[package]]
name = "humanfriendly" name = "humanfriendly"
version = "10.0" version = "10.0"
@ -66,14 +39,6 @@ category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]] [[package]]
name = "svgwrite" name = "svgwrite"
version = "1.4.2" version = "1.4.2"
@ -93,21 +58,13 @@ python-versions = ">= 3.5"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "11298c8670e03d4235a76555b1ca7e6cbf0740041f14c14fdf409f633e197bf5" content-hash = "b989d535550aaf74b32cd9d2ff48ab7ed8d7d3fd5f0386bc2d6385d65adbf438"
[metadata.files] [metadata.files]
anytree = [
{file = "anytree-2.8.0-py2.py3-none-any.whl", hash = "sha256:14c55ac77492b11532395049a03b773d14c7e30b22aa012e337b1e983de31521"},
{file = "anytree-2.8.0.tar.gz", hash = "sha256:3f0f93f355a91bc3e6245319bf4c1d50e3416cc7a35cc1133c1ff38306bbccab"},
]
coloredlogs = [ coloredlogs = [
{file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"},
{file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"},
] ]
filelock = [
{file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"},
{file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"},
]
humanfriendly = [ humanfriendly = [
{file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"},
{file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"},
@ -120,10 +77,6 @@ pyreadline3 = [
{file = "pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb"}, {file = "pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb"},
{file = "pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae"}, {file = "pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae"},
] ]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
svgwrite = [ svgwrite = [
{file = "svgwrite-1.4.2-py3-none-any.whl", hash = "sha256:ca63d76396d1f6f099a2b2d8cf1419e1c1de8deece9a2b7f4da0632067d71d43"}, {file = "svgwrite-1.4.2-py3-none-any.whl", hash = "sha256:ca63d76396d1f6f099a2b2d8cf1419e1c1de8deece9a2b7f4da0632067d71d43"},
{file = "svgwrite-1.4.2.zip", hash = "sha256:d304a929f197d31647c287c10eee0f64378058e1c83a9df83433a5980864e59f"}, {file = "svgwrite-1.4.2.zip", hash = "sha256:d304a929f197d31647c287c10eee0f64378058e1c83a9df83433a5980864e59f"},

View File

@ -10,8 +10,6 @@ tornado = "^6.1"
coloredlogs = "^15.0.1" coloredlogs = "^15.0.1"
pydub = "^0.25.1" pydub = "^0.25.1"
svgwrite = "^1.4.1" svgwrite = "^1.4.1"
anytree = "^2.8.0"
filelock = "^3.7.1"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]