6
0
Pārlūkot izejas kodu

Merge branch 'master' into ammod

Dimitri Korsch 3 gadi atpakaļ
vecāks
revīzija
0cd8b0d1ea

+ 14 - 9
labels/lepiforum_version_7/Provider.py

@@ -32,20 +32,25 @@ class Provider(LabelProvider):
                              entries)
 
         # create result set
+        if self.csv_all_hierarchy_levels:
+            hierarchy_levels = (('superfamily', 'Überfamilie'),
+                                ('family', 'Familie'),
+                                ('subfamily', 'Unterfamilie'),
+                                ('tribe', 'Tribus'),
+                                ('genus', 'Gattung'))
+        else:
+            hierarchy_levels = (('family', 'Familie'),
+                                ('genus', 'Gattung'))
+
         for entry in entries:
             entry = entry.__dict__
             parent_reference = None
 
             # add hierarchy
-            if self.csv_all_hierarchy_levels:
-                hierarchy_levels = ('superfamily', 'family', 'subfamily', 'tribe', 'genus')
-            else:
-                hierarchy_levels = ('family', 'genus')
-
-            for tax in hierarchy_levels:
-                if entry[tax] is not None:
-                    reference, name = entry[tax].lower(), entry[tax]
-                    result.append(self.create_label(reference, name, parent_reference))
+            for level, level_name in hierarchy_levels:
+                if entry[level] is not None:
+                    reference, name = entry[level].lower(), entry[level]
+                    result.append(self.create_label(reference, name, parent_reference, level_name))
 
                     parent_reference = reference
 

+ 28 - 0
migrations/versions/7bbf16ab27b2_.py

@@ -0,0 +1,28 @@
+"""empty message
+
+Revision ID: 7bbf16ab27b2
+Revises: ece44a2b50ef
+Create Date: 2021-08-04 10:56:25.507092
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '7bbf16ab27b2'
+down_revision = 'ece44a2b50ef'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.add_column('label', sa.Column('hierarchy_level', sa.String(), nullable=True))
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_column('label', 'hierarchy_level')
+    # ### end Alembic commands ###

+ 2 - 0
pycs/database/Label.py

@@ -39,6 +39,7 @@ class Label(NamedBaseModel):
         index=True, nullable=False)
 
     reference = db.Column(db.String)
+    hierarchy_level = db.Column(db.String)
 
     # contraints
     __table_args__ = (
@@ -63,6 +64,7 @@ class Label(NamedBaseModel):
         "project_id",
         "parent_id",
         "reference",
+        "children",
     )
 
     def set_parent(self, parent_id: int, commit: bool = True):

+ 46 - 28
pycs/database/Project.py

@@ -60,61 +60,60 @@ class Project(NamedBaseModel):
         "data_folder",
     )
 
-    def label(self, id: int) -> T.Optional[Label]:
+    def file(self, id: int) -> T.Optional[File]:
         """
-        get a label using its unique identifier
+        get a file using its unique identifier
 
         :param id: unique identifier
-        :return: label
+        :return: file
+
         """
+        return self.files.filter_by(id=id).one_or_none()
 
-        return self.labels.filter_by(id=id).one_or_none()
 
-    def label_by_reference(self, reference: str) -> T.Optional[Label]:
+    def label_tree(self) -> T.List[Label]:
         """
-        get a label using its reference string
+        get a list of root labels associated with this project
 
-        :param reference: reference string
-        :return: label
+        :return: list of labels
         """
-        return self.labels.filter_by(reference=reference).one_or_none()
+        return self.labels.filter(Label.parent_id == None).all()
+
 
-    def file(self, id: int) -> T.Optional[Label]:
+    def label(self, id: int) -> T.Optional[Label]:
         """
-        get a file using its unique identifier
+        get a label using its unique identifier
 
         :param id: unique identifier
-        :return: file
+        :return: label
         """
-        return self.files.filter_by(id=id).one_or_none()
 
-    def collection(self, id: int) -> T.Optional[Collection]:
-        """
-        get a collection using its unique identifier
+        return self.labels.filter_by(id=id).one_or_none()
 
-        :param identifier: unique identifier
-        :return: collection
-        """
-        return self.collections.filter_by(id=id).one_or_none()
 
-    def collection_by_reference(self, reference: str) -> T.Optional[Collection]:
+    def label_by_reference(self, reference: str) -> T.Optional[Label]:
         """
-        get a collection using its unique identifier
+        get a label using its reference string
 
-        :param identifier: unique identifier
-        :return: collection
+        :param reference: reference string
+        :return: label
         """
-        return self.collections.filter_by(reference=reference).one_or_none()
+        return self.labels.filter_by(reference=reference).one_or_none()
 
-    def create_label(self, name: str, reference: str = None,
-                     parent_id: T.Union[Label, int, str] = None, commit: bool = True) -> T.Tuple[T.Optional[Label], bool]:
+
+    def create_label(self, name: str,
+                     reference: T.Optional[str] = None,
+                     parent_id: T.Union[Label, int, str] = None,
+                     hierarchy_level: T.Optional[str] = None,
+                     commit: bool = True) -> T.Tuple[T.Optional[Label], bool]:
         """
         create a label for this project. If there is already a label with the same reference
         in the database its name is updated.
 
         :param name: label name
         :param reference: label reference
-        :param parent_id: parent's identifier
+        :param parent: either parent identifier, parent reference string or `Label` object
+        :param hierarchy_level: hierarchy level name
         :return: created or edited label, insert
         """
 
@@ -127,6 +126,7 @@ class Project(NamedBaseModel):
         label, is_new = Label.get_or_create(project=self, reference=reference)
 
         label.name = name
+        label.hierarchy_level = hierarchy_level
         label.set_parent(parent_id, commit=False)
 
         if commit:
@@ -134,6 +134,24 @@ class Project(NamedBaseModel):
 
         return label, is_new
 
+    def collection(self, id: int) -> T.Optional[Collection]:
+        """
+        get a collection using its unique identifier
+
+        :param identifier: unique identifier
+        :return: collection
+        """
+        return self.collections.filter_by(id=id).one_or_none()
+
+    def collection_by_reference(self, reference: str) -> T.Optional[Collection]:
+        """
+        get a collection using its unique identifier
+
+        :param identifier: unique identifier
+        :return: collection
+        """
+        return self.collections.filter_by(reference=reference).one_or_none()
+
     def create_collection(self,
                           reference: str,
                           name: str,

+ 5 - 0
pycs/frontend/WebServer.py

@@ -29,6 +29,7 @@ from pycs.frontend.endpoints.jobs.RemoveJob import RemoveJob
 from pycs.frontend.endpoints.labels.CreateLabel import CreateLabel
 from pycs.frontend.endpoints.labels.EditLabelName import EditLabelName
 from pycs.frontend.endpoints.labels.EditLabelParent import EditLabelParent
+from pycs.frontend.endpoints.labels.ListLabelTree import ListLabelTree
 from pycs.frontend.endpoints.labels.RemoveLabel import RemoveLabel
 from pycs.frontend.endpoints.pipelines.FitModel import FitModel
 from pycs.frontend.endpoints.pipelines.PredictFile import PredictFile
@@ -199,6 +200,10 @@ class WebServer:
             '/projects/<int:project_id>/labels',
             view_func=ListView.as_view('list_labels', model_cls=Label)
         )
+        self.app.add_url_rule(
+            '/projects/<int:identifier>/labels/tree',
+            view_func=ListLabelTree.as_view('list_label_tree')
+        )
         self.app.add_url_rule(
             '/projects/<int:identifier>/labels',
             view_func=CreateLabel.as_view('create_label')

+ 22 - 0
pycs/frontend/endpoints/labels/ListLabelTree.py

@@ -0,0 +1,22 @@
+from flask import abort, jsonify
+from flask.views import View
+
+from pycs.database.Label import Label
+from pycs.database.Project import Project
+
+
+class ListLabelTree(View):
+    """
+    return a list of labels for a given project
+    """
+    # pylint: disable=arguments-differ
+    methods = ['GET']
+
+
+    def dispatch_request(self, identifier):
+
+        project = Project.query.get(identifier)
+        if project is None:
+            return abort(404)
+
+        return jsonify(project.label_tree())

+ 6 - 2
pycs/interfaces/LabelProvider.py

@@ -33,17 +33,21 @@ class LabelProvider:
         raise NotImplementedError
 
     @staticmethod
-    def create_label(reference: str, name: str, parent_id: Optional[int] = None):
+    def create_label(reference: str, name: str,
+                     parent_id: str = None, hierarchy_level: str = None):
         """
         create a label result
 
         :param reference: label reference
         :param name: label name
         :param parent_id: parent's identifier
+        :param hierarchy_level: hierarchy level name
         :return:
         """
         return {
             'name': name,
             'reference': reference,
-            'parent_id': parent_id
+            'parent_id': parent_id,
+            'hierarchy_level': hierarchy_level,
+
         }

+ 5 - 0
webui/src/assets/icons/codescan.svg

@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
+    <path d="M8.47 4.97a.75.75 0 000 1.06L9.94 7.5 8.47 8.97a.75.75 0 101.06 1.06l2-2a.75.75 0 000-1.06l-2-2a.75.75 0 00-1.06 0zM6.53 6.03a.75.75 0 00-1.06-1.06l-2 2a.75.75 0 000 1.06l2 2a.75.75 0 101.06-1.06L5.06 7.5l1.47-1.47z"></path>
+    <path fill-rule="evenodd"
+          d="M12.246 13.307a7.5 7.5 0 111.06-1.06l2.474 2.473a.75.75 0 11-1.06 1.06l-2.474-2.473zM1.5 7.5a6 6 0 1110.386 4.094.75.75 0 00-.292.293A6 6 0 011.5 7.5z"></path>
+</svg>

+ 36 - 4
webui/src/components/media/annotated-image.vue

@@ -8,13 +8,19 @@
                    @filter="filter = $event"
                    :label="label"
                    @label="label = $event"
-                   :labels="labels"/>
+                   :labels="labels"
+                   :zoomBox="zoomBox"
+                   @unzoom="zoomBox=false; interaction=false"
+                   @prevzoom="$refs.overlay.prevZoom()"
+                   @nextzoom="$refs.overlay.nextZoom()"/>
 
       <div class="media">
         <!-- image -->
         <img v-if="current.type === 'image'"
              ref="media" :src="src" alt="media"
-             v-on:load="change" v-on:loadedmetadata="change" v-on:loadeddata="change">
+             :style="cropPosition"
+             v-on:load="change" v-on:loadedmetadata="change" v-on:loadeddata="change"
+             v-on:transitionend="resize">
 
         <!-- video -->
         <template v-if="current.type === 'video'">
@@ -31,7 +37,8 @@
         </template>
 
         <!-- overlay -->
-        <annotation-overlay :file="current"
+        <annotation-overlay ref="overlay"
+                            :file="current"
                             :position="overlayPosition"
                             :size="image"
                             :interaction="interaction"
@@ -39,7 +46,9 @@
                             :label="label"
                             :video="video"
                             :results="results"
-                            :labels="labels"/>
+                            :labels="labels"
+                            :zoom="zoomBox"
+                            @zoom="zoomBox = $event"/>
       </div>
     </template>
   </div>
@@ -114,6 +123,7 @@ export default {
         frame: 0,
         play: false
       },
+      zoomBox: false,
       supported: {
         labeledImages: false,
         boundingBoxes: false,
@@ -145,6 +155,21 @@ export default {
         width: this.image.width + 'px',
         height: this.image.height + 'px'
       }
+    },
+    cropPosition: function () {
+      if (!this.zoomBox)
+        return {
+          transform: ``,
+        };
+
+      const posX = 0.5 - (this.zoomBox.x + this.zoomBox.w / 2);
+      const posY = 0.5 - (this.zoomBox.y + this.zoomBox.h / 2);
+      const factor = 0.75 / Math.max(this.zoomBox.w, this.zoomBox.h);
+
+      // use a transition to use the transitionend event to recalculate box positions
+      return {
+        transform: `scale(${factor}) translateX(${posX * 100}%) translateY(${posY * 100}%)`
+      };
     }
   },
   methods: {
@@ -264,6 +289,8 @@ export default {
         this.video.play = false;
         this.video.frame = 0;
 
+        this.zoomBox = false;
+
         this.$root.socket.get(`/data/${newVal.id}/results`)
             .then(response => response.json())
             .then(results => {
@@ -284,6 +311,8 @@ export default {
   flex-direction: row;
   justify-content: center;
   align-items: center;
+
+  overflow: hidden;
 }
 
 .options-bar {
@@ -299,6 +328,8 @@ export default {
   flex-direction: column;
   justify-content: center;
   align-items: center;
+
+  overflow: hidden;
 }
 
 .video-player {
@@ -315,5 +346,6 @@ export default {
 img, video {
   max-width: 100%;
   max-height: 100%;
+  transition: transform 0.01s;
 }
 </style>

+ 12 - 4
webui/src/components/media/annotation-box.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="annotation-box"
        :style="style"
-       :class="{draggable: draggable}"
+       :class="{draggable: draggable, shine: shine}"
        @mousedown.prevent="click" @touchstart.prevent="click">
     <template v-if="draggable">
       <div class="nw" @mousedown.prevent.stop="resize('nw')" @touchstart.prevent.stop="resize('nw')"/>
@@ -14,8 +14,7 @@
       <div class="se" @mousedown.prevent.stop="resize('se')" @touchstart.prevent.stop="resize('se')"/>
     </template>
 
-    <div v-if="labelName"
-         class="label">
+    <div v-if="labelName" class="label">
       {{ labelName }}
     </div>
   </div>
@@ -24,7 +23,7 @@
 <script>
 export default {
   name: "annotation-box",
-  props: ['box', 'position', 'draggable', 'deletable', 'taggable', 'confirmable', 'labels'],
+  props: ['box', 'position', 'draggable', 'deletable', 'taggable', 'confirmable', 'zoomable', 'labels', 'shine'],
   computed: {
     labelName: function () {
       if (!this.box || !this.box.label_id)
@@ -87,6 +86,10 @@ export default {
         });
         event.stopPropagation();
       }
+      if (this.zoomable) {
+        this.$emit('zoom', this.position);
+        event.stopPropagation();
+      }
     },
     resize: function (mode) {
       this.$emit('resize', mode, this.position, this.update);
@@ -106,6 +109,11 @@ export default {
   border: 1px solid transparent;
 }
 
+.annotation-box.shine {
+  background: none !important;
+  border-width: 5px;
+}
+
 .label {
   position: absolute;
   bottom: 0.25rem;

+ 28 - 4
webui/src/components/media/annotation-overlay.vue

@@ -4,7 +4,7 @@
        @mousedown="press" @mousemove="track" @mouseup="release"
        @dragstart.stop>
 
-    <annotation-box v-for="box in boundingBoxes"
+    <annotation-box v-for="(box, index) in boundingBoxes"
                     v-bind:key="box.id"
                     :box="box"
                     :position="box.data"
@@ -12,9 +12,12 @@
                     :deletable="interaction === 'remove-box'"
                     :taggable="interaction === 'label-box' ? label : false"
                     :confirmable="interaction === 'confirm-box'"
+                    :zoomable="interaction === 'zoom-box'"
                     :labels="labels"
+                    :shine="zoom"
                     @move="move"
-                    @resize="resize"/>
+                    @resize="resize"
+                    @zoom="zoomBox(index, $event)"/>
 
     <annotation-box v-if="current"
                     :position="current"/>
@@ -36,14 +39,15 @@ import AnnotationBox from "@/components/media/annotation-box";
 export default {
   name: "annotation-overlay",
   components: {AnnotationBox},
-  props: ['file', 'position', 'size', 'interaction', 'filter', 'label', 'video', 'results', 'labels'],
+  props: ['file', 'position', 'size', 'interaction', 'filter', 'label', 'video', 'results', 'labels', 'zoom'],
   data: function () {
     return {
       callback: false,
       start: false,
       fixed: false,
       current: false,
-      mousedown: false
+      mousedown: false,
+      zoomed: false
     }
   },
   computed: {
@@ -250,6 +254,26 @@ export default {
           this.fixed = {lx: position.x, hx: position.x + position.w};
           break;
       }
+    },
+    zoomBox: function (index) {
+      if (this.boundingBoxes.length === 0) {
+        this.zoomed = false;
+        return;
+      }
+      if (index < 0) {
+        index = this.boundingBoxes.length - 1;
+      } else if (index >= this.boundingBoxes.length) {
+        index = 0;
+      }
+
+      this.zoomed = index;
+      this.$emit('zoom', this.boundingBoxes[index].data);
+    },
+    prevZoom: function () {
+      this.zoomBox(this.zoomed - 1);
+    },
+    nextZoom: function () {
+      this.zoomBox(this.zoomed + 1);
     }
   },
   watch: {

+ 31 - 2
webui/src/components/media/label-selector.vue

@@ -8,7 +8,12 @@
            class="label selectable"
            :class="{selected: index === selectedIndex}"
            @click="select(label)">
-        {{ label.name }}
+        <div class="hierarchy" v-if="label.hierarchy_level">
+          {{ label.hierarchy_level }}
+        </div>
+        <div class="name">
+          {{ label.name }}
+        </div>
       </div>
     </div>
 
@@ -45,13 +50,32 @@ export default {
     }
   },
   computed: {
+    sortedLabels: function () {
+      return [...this.labels].sort((a, b) => {
+        if (a.hierarchy_level !== b.hierarchy_level) {
+          if (a.hierarchy_level === null)
+            return -1;
+          if (b.hierarchy_level === null)
+            return +1;
+          if (a.hierarchy_level < b.hierarchy_level)
+            return -1;
+          else
+            return +1;
+        }
+
+        if (a.name < b.name)
+          return -1;
+        else
+          return +1;
+      });
+    },
     results: function () {
       const search = this.search.toLowerCase();
 
       return [{
         identifier: null,
         name: 'None'
-      }, ...this.labels]
+      }, ...this.sortedLabels]
           .filter(l => !this.search || !l.id || l.name.toLowerCase().includes(search));
     }
   },
@@ -129,4 +153,9 @@ input {
   background-color: var(--primary);
   padding: 0.25rem 0.5rem;
 }
+
+.hierarchy {
+  font-size: 77%;
+  opacity: 0.8;
+}
 </style>

+ 37 - 8
webui/src/components/media/options-bar.vue

@@ -61,9 +61,33 @@
 
     <div class="spacer"/>
 
+    <div ref="zoom_annotation"
+         class="image"
+         title="zoom bounding box (X)"
+         :class="{active: interaction === 'zoom-box' || zoomBox}"
+         @click="$emit(zoomBox ? 'unzoom' : 'interaction', 'zoom-box')">
+      <img alt="zoom bounding box" src="@/assets/icons/codescan.svg">
+    </div>
+
+    <div ref="zoom_prev_annotation"
+         class="image"
+         title="zoom previous bounding box (C)"
+         @click="$emit('prevzoom', true)">
+      <img alt="zoom previous bounding box" src="@/assets/icons/chevron-left.svg">
+    </div>
+
+    <div ref="zoom_next_annotation"
+         class="image"
+         title="zoom next bounding box (V)"
+         @click="$emit('nextzoom', true)">
+      <img alt="zoom next bounding box" src="@/assets/icons/chevron-right.svg">
+    </div>
+
+    <div class="spacer"/>
+
     <div ref="show_user_annotations"
          class="image"
-         title="show user annotations (X)"
+         title="show user annotations"
          :class="{active: filter && filter.includes('user')}"
          @click="invertFilter('user')">
       <img alt="show user annotations" src="@/assets/icons/smiley.svg">
@@ -71,7 +95,7 @@
 
     <div ref="show_pipeline_annotations"
          class="image"
-         title="show pipeline annotations (C)"
+         title="show pipeline annotations"
          :class="{active: filter && filter.includes('pipeline')}"
          @click="invertFilter('pipeline')">
       <img alt="show pipeline annotations" src="@/assets/icons/cpu.svg">
@@ -95,7 +119,7 @@ import LabelSelector from "@/components/media/label-selector";
 export default {
   name: "options-bar",
   components: {LabelSelector},
-  props: ['file', 'interaction', 'filter', 'label', 'labels'],
+  props: ['file', 'interaction', 'filter', 'label', 'labels', 'zoomBox'],
   created: function () {
     // get data
     this.getJobs();
@@ -179,14 +203,17 @@ export default {
         case 'g':
           this.$refs.confirm_box.click();
           break;
+        case 'b':
+          this.$refs.create_predictions.click();
+          break;
         case 'x':
-          this.$refs.show_user_annotations.click();
+          this.$refs.zoom_annotation.click();
           break;
         case 'c':
-          this.$refs.show_pipeline_annotations.click();
+          this.$refs.zoom_prev_annotation.click();
           break;
         case 'v':
-          this.$refs.create_predictions.click();
+          this.$refs.zoom_next_annotation.click();
           break;
       }
     },
@@ -227,7 +254,9 @@ export default {
 
 <style scoped>
 .options-bar {
-  background-color: rgba(0, 0, 0, 0.25);
+  /* background-color: rgba(0, 0, 0, 0.25); */
+  background-color: #858585;
+  z-index: 1;
 }
 
 .spacer {
@@ -266,7 +295,7 @@ img {
   left: 50%;
   z-index: 101;
 
-  width: 20rem;
+  width: 30rem;
   max-width: 90vw;
   max-height: 90vh;
 

+ 37 - 9
webui/src/components/other/LabelTreeView.vue

@@ -7,18 +7,26 @@
     <editable-headline :value="label.name"
                        @change="editLabel(label.id, $event)"
                        @remove="removeLabel(label.id)">
-      <img v-if="collapse"
-           src="@/assets/icons/triangle-down.svg"
-           :style="{opacity: label.children.length > 0 ? 1 : 0.1}"
-           @click="collapse = false">
-      <img v-else
-           src="@/assets/icons/triangle-up.svg"
-           :style="{opacity: label.children.length > 0 ? 1 : 0.1}"
-           @click="collapse = true">
+      <div class="hierarchy"
+           :class="{margined: label.hierarchy_level}"
+           @click="collapse = !collapse">
+        <img v-if="collapse"
+             alt="expand"
+             src="@/assets/icons/triangle-down.svg"
+             :style="{opacity: label.children.length > 0 ? 1 : 0.1}">
+        <img v-else
+             alt="collapse"
+             src="@/assets/icons/triangle-up.svg"
+             :style="{opacity: label.children.length > 0 ? 1 : 0.1}">
+
+        <div class="name" v-if="label.hierarchy_level">
+          {{ label.hierarchy_level }}
+        </div>
+      </div>
     </editable-headline>
 
     <template v-if="!collapse">
-      <label-tree-view v-for="child of label.children" :key="child.id"
+      <label-tree-view v-for="child of sortedChildren" :key="child.id"
                        :label="child"
                        :indent="indent"
                        :targetable="droppable"
@@ -49,6 +57,9 @@ export default {
       return {
         marginLeft: this.indent
       };
+    },
+    sortedChildren: function () {
+      return [...this.label.children].sort((a, b) => a.name < b.name ? -1 : +1);
     }
   },
   methods: {
@@ -100,4 +111,21 @@ export default {
 .target > .editable-headline {
   text-decoration: underline;
 }
+
+.hierarchy {
+  display: flex;
+  flex-direction: column;
+  justify-content: right;
+  align-items: center;
+}
+
+.hierarchy.margined {
+  margin-right: 0.3rem;
+}
+
+.hierarchy .name {
+  margin-top: -0.5rem;
+  opacity: 0.8;
+  font-size: 50%;
+}
 </style>

+ 73 - 81
webui/src/components/projects/project-labels-window.vue

@@ -7,7 +7,7 @@
     </h1>
 
     <template v-if="labels !== false">
-      <label-tree-view v-for="label in labelTree" :key="label.id"
+      <label-tree-view v-for="label in sortedLabels" :key="label.id"
                        :label="label"
                        :targetable="true"
                        indent="2rem"/>
@@ -45,15 +45,15 @@ export default {
 
     // subscribe to changes
     this.$root.socket.on('connect', this.getLabels);
-    this.$root.socket.on('create-label', this.addLabelToList);
-    this.$root.socket.on('remove-label', this.removeLabelFromList);
-    this.$root.socket.on('edit-label', this.editLabelInList);
+    this.$root.socket.on('create-label', this.addLabel);
+    this.$root.socket.on('remove-label', this.removeLabel);
+    this.$root.socket.on('edit-label', this.editLabel);
   },
   destroyed: function () {
     this.$root.socket.off('connect', this.getLabels);
-    this.$root.socket.off('create-label', this.addLabelToList);
-    this.$root.socket.off('remove-label', this.removeLabelFromList);
-    this.$root.socket.off('edit-label', this.editLabelInList);
+    this.$root.socket.off('create-label', this.addLabel);
+    this.$root.socket.off('remove-label', this.removeLabel);
+    this.$root.socket.off('edit-label', this.editLabel);
   },
   data: function () {
     return {
@@ -63,85 +63,95 @@ export default {
     }
   },
   computed: {
-    // TODO remove
     sortedLabels: function () {
       return [...this.labels].sort((a, b) => a.name < b.name ? -1 : +1);
-    },
-    labelTree: function () {
-      const labels = [...this.labels].sort((a, b) => a.name < b.name ? +1 : -1);
-
-      const tree = [];
-      const references = {};
-
-      while (labels.length > 0) {
-        const length = labels.length;
-
-        for (let i = labels.length - 1; i >= 0; i--) {
-          if (labels[i]['parent_id'] === null) {
-            const label = Object.assign({
-              // parent: null,
-              children: []
-            }, labels.splice(i, 1)[0]);
-
-            tree.push(label);
-            references[label.id] = label;
-          } else if (labels[i]['parent_id'] in references) {
-            const parent = references[labels[i]['parent_id']];
-            const label = Object.assign({
-              // parent: parent,
-              children: []
-            }, labels.splice(i, 1)[0]);
-
-            parent.children.push(label);
-            references[label.id] = label;
-          }
-        }
-
-        if (labels.length === length) {
-          // TODO show in ui
-          console.log('I could not parse all items. Sorry!')
-          return tree;
-        }
-      }
-
-      return tree;
     }
   },
   methods: {
     getLabels: function () {
-      this.$root.socket.get(`/projects/${this.$root.project.id}/labels`)
+      this.$root.socket.get(`/projects/${this.$root.project.id}/labels/tree`)
           .then(response => response.json())
           .then(labels => {
-            this.labels = [];
-            labels.forEach(this.addLabelToList);
+            this.labels = labels;
           });
     },
-    addLabelToList: function (label) {
+    findLabel: function (labels, id) {
+      for (let label of labels) {
+        if (label.id === id)
+          return label;
+
+        const child = this.findLabel(label.children, id);
+        if (child)
+          return child;
+      }
+
+      return null;
+    },
+    addLabel: function (label) {
+      // abort if label is not part of this project
       if (label['project_id'] !== this.$root.project.id)
         return;
 
-      for (let l of this.labels)
+      // prepare some values
+      let labels = this.labels;
+
+      if (!label.children)
+        label.children = [];
+
+      // label is not on root level
+      if (label.parent_id !== null) {
+        const parent = this.findLabel(labels, label.parent_id);
+
+        if (parent) {
+          labels = parent.children;
+        } else {
+          console.warn('could not find parent');
+          return;
+        }
+      }
+
+      // abort if label is already contained
+      for (let l of labels)
         if (l.id === label.id)
           return;
 
-      this.labels.push(label);
+      // add label
+      labels.push(label);
     },
-    removeLabelFromList: function (label) {
-      for (let i = 0; i < this.labels.length; i++) {
-        if (this.labels[i].id === label.id) {
-          this.labels.splice(i, 1);
+    removeLabel: function (label) {
+      let labels = this.labels;
+
+      // label is not on root level
+      if (label.parent_id !== null) {
+        const parent = this.findLabel(labels, label.parent_id);
+
+        if (parent) {
+          labels = parent.children;
+        } else {
+          console.warn('could not find parent');
           return;
         }
       }
-    },
-    editLabelInList: function (label) {
-      for (let i = 0; i < this.labels.length; i++) {
-        if (this.labels[i].id === label.id) {
-          this.$set(this.labels, i, label);
+
+      // remove label from list
+      for (let i = 0; i < labels.length; i++) {
+        if (labels[i].id === label.id) {
+          labels.splice(i, 1);
           return;
         }
       }
     },
+    editLabel: function (label) {
+      // remove label if it is already contained
+      const l = this.findLabel(this.labels, label.id);
+      if (l) {
+        this.removeLabel(l);
+        label.children = l.children;
+      }
+
+      // add label on it's new position
+      this.addLabel(label);
+    },
     createLabel: function () {
       if (!this.createLabelValue)
         return;
@@ -160,7 +170,7 @@ export default {
     drop: function (e) {
       this.dragleave();
 
-      const element = e.dataTransfer.getData('text/identifier');
+      const element = e.dataTransfer.getData('text/id');
       this.$root.socket.post(`/projects/${this.$root.project.id}/labels/${element}/parent`, {parent: null});
     }
   }
@@ -181,22 +191,4 @@ h1.target {
   width: 4rem;
   height: 4rem;
 }
-
-/*
-/deep/ .element {
-  border: none;
-}
-
-/deep/ .element:not(:first-child) {
-  margin-left: 0.1rem;
-}
-
-/deep/ .element:not(:last-child) {
-  margin-right: 0.1rem;
-}
-
-.no-elements {
-  margin-top: 0.5rem;
-}
-*/
 </style>