WIP upgraded index, slash annotation editor
This commit is contained in:
parent
ab9c66794c
commit
5fa5096c44
5 changed files with 382 additions and 124 deletions
|
@ -728,6 +728,9 @@ class Tag(NodeMixin):
|
|||
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()
|
||||
|
@ -742,11 +745,17 @@ class Tag(NodeMixin):
|
|||
return t
|
||||
return None
|
||||
|
||||
def toJson(self) -> str:
|
||||
return JsonExporter(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)
|
||||
|
||||
# print(tree.descendants)
|
||||
return tree
|
||||
|
||||
# print(RenderTree(tree))
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
}
|
||||
|
||||
#tags li {
|
||||
|
||||
line-height: 1.5;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
|
@ -33,20 +33,31 @@
|
|||
|
||||
#tags li>div,
|
||||
#tags li.add-tag {
|
||||
cursor: pointer
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#tags .tag-id-root>div {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#tags li:hover>ul>li.add-tag {
|
||||
/* #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;
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#tags .add-tag {
|
||||
visibility: hidden;
|
||||
display: none;
|
||||
;
|
||||
color: lightgray;
|
||||
font-size: 80%;
|
||||
|
@ -59,9 +70,30 @@
|
|||
padding: 0;
|
||||
border: solid 1px black;
|
||||
border-radius: 2px;
|
||||
vertical-align: bottom;
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
}
|
||||
#tags li:hover > div > input.rm-tag{
|
||||
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
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import tornado.ioloop
|
||||
import tornado.web
|
||||
import tornado.websocket
|
||||
|
@ -519,7 +520,34 @@ class TagsHandler(tornado.web.RequestHandler):
|
|||
self.index = index
|
||||
|
||||
def get(self):
|
||||
raise Exception('todo')
|
||||
self.set_header("Content-Type", "application/json")
|
||||
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)
|
||||
|
||||
self.set_status(204)
|
||||
# print()
|
||||
|
||||
|
||||
class IndexHandler(tornado.web.RequestHandler):
|
||||
"""Get annotation as svg"""
|
||||
|
@ -603,6 +631,9 @@ class Server:
|
|||
(r"/index", IndexHandler,
|
||||
{"config": self.config, "index": self.index}),
|
||||
|
||||
(r"/tags.json", TagsHandler,
|
||||
{"config": self.config, "index": self.index}),
|
||||
|
||||
(r"/(.*)", StaticFileWithHeaderHandler,
|
||||
{"path": self.web_root, 'default_filename': 'index.html'}),
|
||||
],
|
||||
|
|
|
@ -6,6 +6,8 @@ class AnnotationManager {
|
|||
this.tagsEl = this.rootEl.querySelector('#tags');
|
||||
this.annotationsEl = this.rootEl.querySelector('#annotations');
|
||||
|
||||
this.selectedAnnotations = [];
|
||||
this.selectedTag = null;
|
||||
this.loadTags(tagsUrl);
|
||||
}
|
||||
|
||||
|
@ -33,6 +35,9 @@ class AnnotationManager {
|
|||
tagEl.addEventListener('click', (ev) => {
|
||||
this.selectTag(tag);
|
||||
})
|
||||
tagEl.addEventListener('dblclick', (ev) => {
|
||||
this.renameTag(tag);
|
||||
})
|
||||
|
||||
const colorEl = document.createElement('input');
|
||||
colorEl.type = 'color';
|
||||
|
@ -42,6 +47,16 @@ class AnnotationManager {
|
|||
});
|
||||
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);
|
||||
|
||||
|
@ -70,14 +85,16 @@ class AnnotationManager {
|
|||
|
||||
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 = "";
|
||||
}
|
||||
|
@ -112,14 +129,33 @@ class AnnotationManager {
|
|||
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('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)
|
||||
|
||||
|
@ -140,6 +176,7 @@ class AnnotationManager {
|
|||
|
||||
liEl.appendChild(imgEl);
|
||||
liEl.appendChild(playerEl);
|
||||
liEl.appendChild(selectEl);
|
||||
liEl.appendChild(infoEl);
|
||||
ulEl.appendChild(liEl);
|
||||
|
||||
|
@ -149,6 +186,76 @@ class AnnotationManager {
|
|||
|
||||
}
|
||||
|
||||
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()}':`);
|
||||
|
@ -156,15 +263,25 @@ class AnnotationManager {
|
|||
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 => response.json())
|
||||
.then(response => {
|
||||
if (response.status == 404) {
|
||||
return [] // not existing tag surely has no annotations
|
||||
} else {
|
||||
return response.json()
|
||||
}
|
||||
})
|
||||
.then(annotations => {
|
||||
if(annotations.length) {
|
||||
if (annotations.length) {
|
||||
alert(`Cannot remove '${tag.get_name()}', as it is used for ${annotations.length} annotations.`)
|
||||
} else {
|
||||
// TODO: remove tag
|
||||
|
@ -172,8 +289,9 @@ class AnnotationManager {
|
|||
tag.parent = null;
|
||||
|
||||
this.saveTags();
|
||||
this.buildTagList();
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
setTagColor(tag, color) {
|
||||
|
@ -183,15 +301,22 @@ class AnnotationManager {
|
|||
this.saveTags();
|
||||
}
|
||||
|
||||
moveSelectedAnnotations(newTag) {
|
||||
moveSelectedAnnotations(tag) {
|
||||
// TODO: add button for this
|
||||
throw new Error("Not implemented");
|
||||
alert(`This doesn't work yet! (move to tag ${tag.get_name()})`)
|
||||
}
|
||||
|
||||
saveTags() {
|
||||
async saveTags() {
|
||||
const json = this.rootTag.export();
|
||||
// TODO: save to remote
|
||||
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'));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -243,14 +368,19 @@ class Tag {
|
|||
}
|
||||
|
||||
get_name() {
|
||||
if (this.hasOwnProperty('name')) {
|
||||
if (this.hasOwnProperty('name') && this.name !== null) {
|
||||
return this.name;
|
||||
}
|
||||
return this.id;
|
||||
}
|
||||
|
||||
get_indented_name() {
|
||||
const name = this.get_name();
|
||||
return ' '.repeat((this.depth() - 1) * 2) + '- ' + name
|
||||
}
|
||||
|
||||
get_color() {
|
||||
if (this.hasOwnProperty('color')) {
|
||||
if (this.hasOwnProperty('color') && this.color !== null) {
|
||||
return this.color;
|
||||
}
|
||||
if (this.parent !== null) {
|
||||
|
|
|
@ -1,116 +1,172 @@
|
|||
{
|
||||
"id": "root",
|
||||
"children": [
|
||||
"id": "root",
|
||||
"name": "root",
|
||||
"color": null,
|
||||
"description": "",
|
||||
"children": [
|
||||
{
|
||||
"id": "human-machine",
|
||||
"name": "Human/machine Entanglements (Appearance/disappearance)",
|
||||
"color": "#ffa348",
|
||||
"description": "",
|
||||
"children": [
|
||||
{
|
||||
"id": "human-machine",
|
||||
"name": "Human/machine Entanglements (Appearance/disappearance)",
|
||||
"color": "orange",
|
||||
"children": [
|
||||
{
|
||||
"id": "vision",
|
||||
"name": "Vision",
|
||||
"color": "orange"
|
||||
},
|
||||
{
|
||||
"id": "sound"
|
||||
},
|
||||
{
|
||||
"id": "behaviour"
|
||||
},
|
||||
{
|
||||
"id": "other-senses",
|
||||
"name": "Other senses"
|
||||
}
|
||||
]
|
||||
"id": "vision",
|
||||
"name": "Vision",
|
||||
"color": null,
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "tensions",
|
||||
"name": "Tensions, contestations & problems",
|
||||
"description": "Which problems are identified?, when do they become problems?",
|
||||
"color": "gray"
|
||||
"id": "sound",
|
||||
"name": "sound",
|
||||
"color": null,
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "security",
|
||||
"color": "blue",
|
||||
"name": "Security & types of data",
|
||||
"children": [
|
||||
{
|
||||
"id": "definitions",
|
||||
"description": "e.g. domain knowledge"
|
||||
},
|
||||
{
|
||||
"id": "input",
|
||||
"description": "e.g. fake data"
|
||||
}
|
||||
]
|
||||
"id": "behaviour",
|
||||
"name": "behaviour",
|
||||
"color": null,
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "actants",
|
||||
"name": "Actants in relation",
|
||||
"color": "pink",
|
||||
"children": [
|
||||
{
|
||||
"id": "algorithm"
|
||||
},
|
||||
{
|
||||
"id": "technologies"
|
||||
},
|
||||
{
|
||||
"id": "frt"
|
||||
},
|
||||
{
|
||||
"id": "cameras",
|
||||
"name": "CCTV & camera's"
|
||||
},
|
||||
{
|
||||
"id": "entities",
|
||||
"name": "Entities: people, institutions etc."
|
||||
},
|
||||
{
|
||||
"id": "positioning",
|
||||
"name": "Positioning",
|
||||
"description": "the positioning of a field/person/oneself in relation to others"
|
||||
},
|
||||
{
|
||||
"id": "inside-outside"
|
||||
},
|
||||
{
|
||||
"id": "public-private"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "consequences",
|
||||
"color": "green",
|
||||
"children": [
|
||||
{
|
||||
"id": "effects"
|
||||
},
|
||||
{
|
||||
"id": "future-imaginaries"
|
||||
},
|
||||
{
|
||||
"id": "speculations",
|
||||
"description": "what is & what will/can be done."
|
||||
},
|
||||
{
|
||||
"id": "innovations"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "hesitation",
|
||||
"name": "Hesitations & corrections",
|
||||
"color": "yellow"
|
||||
},
|
||||
{
|
||||
"id": "skip",
|
||||
"color": "black"
|
||||
},
|
||||
{
|
||||
"id": "todo",
|
||||
"name": "to do / interesting",
|
||||
"color": "red"
|
||||
"id": "other-senses",
|
||||
"name": "Other senses",
|
||||
"color": null,
|
||||
"description": ""
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "tensions",
|
||||
"name": "Tensions, contestations & problems",
|
||||
"color": "#77767b",
|
||||
"description": "Which problems are identified?, when do they become problems?"
|
||||
},
|
||||
{
|
||||
"id": "security",
|
||||
"name": "Security & types of data",
|
||||
"color": "#3584e4",
|
||||
"description": "",
|
||||
"children": [
|
||||
{
|
||||
"id": "definitions",
|
||||
"name": "definitions",
|
||||
"color": null,
|
||||
"description": "e.g. domain knowledge"
|
||||
},
|
||||
{
|
||||
"id": "input",
|
||||
"name": "input",
|
||||
"color": null,
|
||||
"description": "e.g. fake data"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "actants",
|
||||
"name": "Actants in relation",
|
||||
"color": "#fa08ff",
|
||||
"description": "",
|
||||
"children": [
|
||||
{
|
||||
"id": "algorithm",
|
||||
"name": "algorithm",
|
||||
"color": null,
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "technologies",
|
||||
"name": "technologies",
|
||||
"color": null,
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "frt",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"id": "inside-outside",
|
||||
"name": "inside-outside",
|
||||
"color": null,
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "public-private",
|
||||
"name": "public-private",
|
||||
"color": null,
|
||||
"description": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "consequences",
|
||||
"name": "consequences",
|
||||
"color": "#0add32",
|
||||
"description": "",
|
||||
"children": [
|
||||
{
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"id": "innovations",
|
||||
"name": "innovations",
|
||||
"color": null,
|
||||
"description": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "hesitation",
|
||||
"name": "Hesitations & corrections",
|
||||
"color": "#f8e45c",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "skip",
|
||||
"name": "skip",
|
||||
"color": null,
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "todo",
|
||||
"name": "to do / interesting",
|
||||
"color": "#ff0000",
|
||||
"description": ""
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Reference in a new issue