Remove old UI (#1964)
* Removed outdated code * Revert changes in .gitignore * Fix docker build * Remove karma + some dummy code * Fix a type in Dockerfile.ci * Fix swagger and remove dispatch_request. * Update CHANGELOG.mdmain
@ -1,28 +0,0 @@
|
||||
# Copyright (C) 2018 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from django.urls import path
|
||||
from django.conf import settings
|
||||
from rest_auth.views import (
|
||||
LoginView, LogoutView, PasswordChangeView,
|
||||
PasswordResetView, PasswordResetConfirmView)
|
||||
from rest_auth.registration.views import RegisterView
|
||||
from .views import SigningView
|
||||
|
||||
urlpatterns = [
|
||||
path('login', LoginView.as_view(), name='rest_login'),
|
||||
path('logout', LogoutView.as_view(), name='rest_logout'),
|
||||
path('signing', SigningView.as_view(), name='signing')
|
||||
]
|
||||
|
||||
if settings.DJANGO_AUTH_TYPE == 'BASIC':
|
||||
urlpatterns += [
|
||||
path('register', RegisterView.as_view(), name='rest_register'),
|
||||
path('password/reset', PasswordResetView.as_view(),
|
||||
name='rest_password_reset'),
|
||||
path('password/reset/confirm', PasswordResetConfirmView.as_view(),
|
||||
name='rest_password_reset_confirm'),
|
||||
path('password/change', PasswordChangeView.as_view(),
|
||||
name='rest_password_change'),
|
||||
]
|
||||
@ -1,60 +0,0 @@
|
||||
|
||||
# Copyright (C) 2018 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from django.contrib.auth.forms import (
|
||||
UsernameField,
|
||||
AuthenticationForm,
|
||||
UserCreationForm,
|
||||
)
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from django import forms
|
||||
|
||||
class AuthForm(AuthenticationForm):
|
||||
username = UsernameField(
|
||||
widget=forms.TextInput(attrs={'autofocus': True, 'placeholder': "Username"}),
|
||||
)
|
||||
password = forms.CharField(
|
||||
label=_("Password"),
|
||||
strip=False,
|
||||
widget=forms.PasswordInput(attrs={'placeholder': "Password"}),
|
||||
)
|
||||
|
||||
class NewUserForm(UserCreationForm):
|
||||
username = UsernameField(
|
||||
widget=forms.TextInput(attrs={'autofocus': True, 'placeholder': "Username (required)"}),
|
||||
required=True,
|
||||
)
|
||||
|
||||
first_name = UsernameField(
|
||||
widget=forms.TextInput(attrs={'placeholder': "First name"}),
|
||||
required=False,
|
||||
)
|
||||
|
||||
last_name = UsernameField(
|
||||
widget=forms.TextInput(attrs={'placeholder': "Last name"}),
|
||||
required=False,
|
||||
)
|
||||
|
||||
email = forms.EmailField(
|
||||
widget=forms.EmailInput(attrs={'placeholder': "Email (required)"}),
|
||||
required=True,
|
||||
)
|
||||
|
||||
password1 = forms.CharField(
|
||||
label=_("Password"),
|
||||
strip=False,
|
||||
widget=forms.PasswordInput(attrs={'placeholder': "Password (required)"}),
|
||||
)
|
||||
password2 = forms.CharField(
|
||||
label=_("Password confirmation"),
|
||||
widget=forms.PasswordInput(attrs={'placeholder': "Password confirmation (required)"}),
|
||||
strip=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('username', 'first_name', 'last_name', 'email', )
|
||||
@ -1,5 +0,0 @@
|
||||
|
||||
# Copyright (C) 2018 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
|
||||
# Copyright (C) 2018 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
<!--
|
||||
Copyright (C) 2018 Intel Corporation
|
||||
|
||||
SPDX-License-Identifier: MIT
|
||||
-->
|
||||
{% extends "auth_base.html" %}
|
||||
|
||||
{% block title %}Forbidden{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Forbidden</h1>
|
||||
{% if user.is_authenticated %}
|
||||
<p align="center">Your account doesn't have access to this page. To proceed,
|
||||
please login with an account that has access or contact your admin.<br>
|
||||
<big><a href="{% url 'login' %}">Login</a><big></p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@ -1,99 +0,0 @@
|
||||
<!--
|
||||
Copyright (c) 2018 by Tyler Fry (https://codepen.io/frytyler/pen/EGdtg)
|
||||
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" >
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
|
||||
<style>
|
||||
.btn { display: inline-block; *display: inline; *zoom: 1; padding: 4px 10px 4px; margin-bottom: 0; font-size: 13px; line-height: 18px; color: #333333; text-align: center;text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); vertical-align: middle; background-color: #f5f5f5; background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); background-image: -ms-linear-gradient(top, #ffffff, #e6e6e6); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); background-image: linear-gradient(top, #ffffff, #e6e6e6); background-repeat: repeat-x; filter: progid:dximagetransform.microsoft.gradient(startColorstr=#ffffff, endColorstr=#e6e6e6, GradientType=0); border-color: #e6e6e6 #e6e6e6 #e6e6e6; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); border: 1px solid #e6e6e6; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); cursor: pointer; *margin-left: .3em; }
|
||||
.btn:hover, .btn:active, .btn.active, .btn.disabled, .btn[disabled] { background-color: #e6e6e6; }
|
||||
.btn-large { padding: 9px 14px; font-size: 15px; line-height: normal; -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; }
|
||||
.btn:hover { color: #333333; text-decoration: none; background-color: #e6e6e6; background-position: 0 -15px; -webkit-transition: background-position 0.1s linear; -moz-transition: background-position 0.1s linear; -ms-transition: background-position 0.1s linear; -o-transition: background-position 0.1s linear; transition: background-position 0.1s linear; }
|
||||
.btn-primary, .btn-primary:hover { text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); color: #ffffff; }
|
||||
.btn-primary.active { color: rgba(255, 255, 255, 0.75); }
|
||||
.btn-primary { background-color: #4a77d4; background-image: -moz-linear-gradient(top, #6eb6de, #4a77d4); background-image: -ms-linear-gradient(top, #6eb6de, #4a77d4); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#6eb6de), to(#4a77d4)); background-image: -webkit-linear-gradient(top, #6eb6de, #4a77d4); background-image: -o-linear-gradient(top, #6eb6de, #4a77d4); background-image: linear-gradient(top, #6eb6de, #4a77d4); background-repeat: repeat-x; filter: progid:dximagetransform.microsoft.gradient(startColorstr=#6eb6de, endColorstr=#4a77d4, GradientType=0); border: 1px solid #3762bc; text-shadow: 1px 1px 1px rgba(0,0,0,0.4); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.5); }
|
||||
.btn-primary:hover, .btn-primary:active, .btn-primary.active, .btn-primary.disabled, .btn-primary[disabled] { filter: none; background-color: #4a77d4; }
|
||||
.btn-block { width: 100%; display:block; }
|
||||
|
||||
* { -webkit-box-sizing:border-box; -moz-box-sizing:border-box; -ms-box-sizing:border-box; -o-box-sizing:border-box; box-sizing:border-box; }
|
||||
|
||||
html { width: 100%; height:100%; overflow:hidden; }
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
height:100%;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
background: #092756;
|
||||
background: -moz-linear-gradient(-45deg, #afcfec 0%, #17458f 100%);
|
||||
background: -webkit-linear-gradient(-45deg, #afcfec 0%,#17458f 100%);
|
||||
}
|
||||
.login {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin: -150px 0 0 -150px;
|
||||
width:300px;
|
||||
height:300px;
|
||||
}
|
||||
.login h1 { color: #fff; text-shadow: 0 0 10px rgba(0,0,0,0.3); letter-spacing:1px; text-align:center; }
|
||||
|
||||
.note {
|
||||
color: #fff;
|
||||
text-shadow: 0 0 10px rgba(0,0,0,0.3);
|
||||
position: absolute;
|
||||
bottom: 10%;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
font-size: large;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
background: rgba(0,0,0,0.3);
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 10px;
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
text-shadow: 1px 1px 1px rgba(0,0,0,0.3);
|
||||
border: 1px solid rgba(0,0,0,0.3);
|
||||
border-radius: 4px;
|
||||
box-shadow: inset 0 -5px 45px rgba(100,100,100,0.2), 0 1px 1px rgba(255,255,255,0.2);
|
||||
-webkit-transition: box-shadow .5s ease;
|
||||
-moz-transition: box-shadow .5s ease;
|
||||
-o-transition: box-shadow .5s ease;
|
||||
-ms-transition: box-shadow .5s ease;
|
||||
transition: box-shadow .5s ease;
|
||||
}
|
||||
input::placeholder {
|
||||
color: rgb(168, 168, 168);
|
||||
}
|
||||
input:focus { box-shadow: inset 0 -5px 45px rgba(100,100,100,0.4), 0 1px 1px rgba(255,255,255,0.2); }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="login">
|
||||
{% block content %} {% endblock %}
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
{% block note %} {% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,29 +0,0 @@
|
||||
<!--
|
||||
Copyright (C) 2018 Intel Corporation
|
||||
|
||||
SPDX-License-Identifier: MIT
|
||||
-->
|
||||
{% extends "auth_base.html" %}
|
||||
|
||||
{% block title %}Login{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Login</h1>
|
||||
{% if form.errors %}
|
||||
<small>Your username and password didn't match. Please try again.</small>
|
||||
{% endif %}
|
||||
<form method="post" action="{% url 'login' %}">
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
<button type="submit" class="btn btn-primary btn-block btn-large">Login</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block note%}
|
||||
{% autoescape off %}
|
||||
{{ note }}
|
||||
{% endautoescape %}
|
||||
{% endblock %}
|
||||
@ -1,32 +0,0 @@
|
||||
<!--
|
||||
Copyright (C) 2018 Intel Corporation
|
||||
|
||||
SPDX-License-Identifier: MIT
|
||||
-->
|
||||
{% extends "auth_base.html" %}
|
||||
|
||||
{% block title %}Create user{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="login">
|
||||
<h1>Create user</h1>
|
||||
<form method="post" action="{% url 'register' %}">
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
{{ field }}
|
||||
{% if field.errors %}
|
||||
{% for error in field.errors %}
|
||||
<small>{{ error }}<small>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
<button type="submit" class="btn btn-primary btn-block btn-large">Register</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -1,21 +0,0 @@
|
||||
<!--
|
||||
Copyright (C) 2018 Intel Corporation
|
||||
|
||||
SPDX-License-Identifier: MIT
|
||||
-->
|
||||
<div class='userProfile'>
|
||||
{% if user.is_authenticated %}
|
||||
{% if user.ldap_user and 'thumbnailPhoto' in user.ldap_user.attrs %}
|
||||
{# TODO insert photo from ldap #}
|
||||
{% endif %}
|
||||
|
||||
{% if user.first_name and user.last_name %}
|
||||
<p>{{ user.first_name }} {{ user.last_name }}</p>
|
||||
{% else %}
|
||||
<p>{{ user.username }}</p>
|
||||
{% endif %}
|
||||
<form action="{% url 'logout' %}">
|
||||
<input type="submit" class="menuButton semiBold h2" value="Logout">
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -1,9 +0,0 @@
|
||||
|
||||
# Copyright (C) 2018 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
@ -1,23 +1,28 @@
|
||||
|
||||
# Copyright (C) 2018 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from django.urls import path
|
||||
from django.contrib.auth import views as auth_views
|
||||
from django.conf import settings
|
||||
|
||||
from . import forms
|
||||
from . import views
|
||||
from rest_auth.views import (
|
||||
LoginView, LogoutView, PasswordChangeView,
|
||||
PasswordResetView, PasswordResetConfirmView)
|
||||
from rest_auth.registration.views import RegisterView
|
||||
from .views import SigningView
|
||||
|
||||
urlpatterns = [
|
||||
path('login', auth_views.LoginView.as_view(form_class=forms.AuthForm,
|
||||
template_name='login.html', extra_context={'note': settings.AUTH_LOGIN_NOTE}),
|
||||
name='login'),
|
||||
path('logout', auth_views.LogoutView.as_view(next_page='login'), name='logout'),
|
||||
path('login', LoginView.as_view(), name='rest_login'),
|
||||
path('logout', LogoutView.as_view(), name='rest_logout'),
|
||||
path('signing', SigningView.as_view(), name='signing')
|
||||
]
|
||||
|
||||
if settings.DJANGO_AUTH_TYPE == 'BASIC':
|
||||
urlpatterns += [
|
||||
path('register', views.register_user, name='register'),
|
||||
path('register', RegisterView.as_view(), name='rest_register'),
|
||||
path('password/reset', PasswordResetView.as_view(),
|
||||
name='rest_password_reset'),
|
||||
path('password/reset/confirm', PasswordResetConfirmView.as_view(),
|
||||
name='rest_password_reset_confirm'),
|
||||
path('password/change', PasswordChangeView.as_view(),
|
||||
name='rest_password_change'),
|
||||
]
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
|
||||
# Copyright (C) 2018 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
|
||||
# Copyright (C) 2018 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
|
||||
# Copyright (C) 2018 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
|
||||
# Copyright (C) 2018 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
@ -1,181 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
html {
|
||||
background-color: #FFFFFF;
|
||||
margin: 0px auto;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: BarlowSemiCondensed-Regular;
|
||||
src: url(fonts/BarlowSemiCondensed-Regular.ttf);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: BarlowSemiCondensed-SemiBold;
|
||||
src: url(fonts/BarlowSemiCondensed-SemiBold.ttf);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: BarlowSemiCondensed-Bold;
|
||||
src: url(fonts/BarlowSemiCondensed-Bold.ttf);
|
||||
}
|
||||
|
||||
.regular {
|
||||
font-family: BarlowSemiCondensed-Regular;
|
||||
}
|
||||
|
||||
.semiBold {
|
||||
font-family: BarlowSemiCondensed-SemiBold;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-family: BarlowSemiCondensed-Bold;
|
||||
}
|
||||
|
||||
.h2 {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.h1 {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.h3 {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: white;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: rgb(0,0,0);
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #FFFFFF;
|
||||
margin: 15% auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #888;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.close:before, .close:after {
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
content: ' ';
|
||||
height: 22px;
|
||||
width: 2px;
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
.close:before {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.close:after {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.tab {
|
||||
overflow: hidden;
|
||||
border: 1px solid black;
|
||||
background-color: #B0C4DE;
|
||||
}
|
||||
|
||||
.tab button {
|
||||
background-color: inherit;
|
||||
float: left;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
padding: 3px 0px;
|
||||
transition: 0.3s;
|
||||
|
||||
}
|
||||
|
||||
.tab button:hover {
|
||||
background-color: #81aee8;
|
||||
}
|
||||
|
||||
.tab button.active {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.activeTabButton {
|
||||
background-color: #81aee8 !important;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectable {
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
position: absolute;
|
||||
background-color: #f1f1f1;
|
||||
min-width: 160px;
|
||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
||||
z-index: 1;
|
||||
list-style-type: none;
|
||||
padding-inline-start: 0;
|
||||
margin-left: 20%;
|
||||
margin-top: -1%;
|
||||
}
|
||||
|
||||
.dropdown-content li {
|
||||
color: black;
|
||||
padding: 12px 16px;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-content li:hover {background-color: #ddd}
|
||||
@ -1,95 +0,0 @@
|
||||
Fonts was downloaded from: https://fonts.google.com/specimen/Barlow+Semi+Condensed
|
||||
|
||||
Copyright 2017 The Barlow Project Authors (https://github.com/jpt/barlow)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@ -1,5 +0,0 @@
|
||||
Icons was downloaded from: https://icomoon.io/#preview-free [GitHub](https://github.com/Keyamoon/IcoMoon-Free)
|
||||
|
||||
[License](https://github.com/Keyamoon/IcoMoon-Free/blob/master/License.txt):
|
||||
|
||||
You can use this package under one of these two licenses: [CC BY 4.0](http://creativecommons.org/licenses/by/4.0/) or [GPL](http://www.gnu.org/licenses/gpl.html).
|
||||
|
Before Width: | Height: | Size: 254 B |
|
Before Width: | Height: | Size: 532 B |
|
Before Width: | Height: | Size: 690 B |
|
Before Width: | Height: | Size: 668 B |
|
Before Width: | Height: | Size: 509 B |
|
Before Width: | Height: | Size: 185 B |
|
Before Width: | Height: | Size: 657 B |
|
Before Width: | Height: | Size: 634 B |
|
Before Width: | Height: | Size: 630 B |
|
Before Width: | Height: | Size: 287 B |
|
Before Width: | Height: | Size: 410 B |
|
Before Width: | Height: | Size: 422 B |
|
Before Width: | Height: | Size: 446 B |
|
Before Width: | Height: | Size: 673 B |
|
Before Width: | Height: | Size: 409 B |
|
Before Width: | Height: | Size: 182 B |
@ -1,184 +0,0 @@
|
||||
/**
|
||||
* @preserve jquery.fullscreen 1.1.5
|
||||
* https://github.com/kayahr/jquery-fullscreen-plugin
|
||||
* Copyright (C) 2012-2013 Klaus Reimer <k@ailis.de>
|
||||
* Licensed under the MIT license
|
||||
* (See http://www.opensource.org/licenses/mit-license)
|
||||
*/
|
||||
|
||||
(function(jQuery) {
|
||||
|
||||
/**
|
||||
* Sets or gets the fullscreen state.
|
||||
*
|
||||
* @param {boolean=} state
|
||||
* True to enable fullscreen mode, false to disable it. If not
|
||||
* specified then the current fullscreen state is returned.
|
||||
* @return {boolean|Element|jQuery|null}
|
||||
* When querying the fullscreen state then the current fullscreen
|
||||
* element (or true if browser doesn't support it) is returned
|
||||
* when browser is currently in full screen mode. False is returned
|
||||
* if browser is not in full screen mode. Null is returned if
|
||||
* browser doesn't support fullscreen mode at all. When setting
|
||||
* the fullscreen state then the current jQuery selection is
|
||||
* returned for chaining.
|
||||
* @this {jQuery}
|
||||
*/
|
||||
function fullScreen(state)
|
||||
{
|
||||
var e, func, doc;
|
||||
|
||||
// Do nothing when nothing was selected
|
||||
if (!this.length) return this;
|
||||
|
||||
// We only use the first selected element because it doesn't make sense
|
||||
// to fullscreen multiple elements.
|
||||
e = (/** @type {Element} */ this[0]);
|
||||
|
||||
// Find the real element and the document (Depends on whether the
|
||||
// document itself or a HTML element was selected)
|
||||
if (e.ownerDocument)
|
||||
{
|
||||
doc = e.ownerDocument;
|
||||
}
|
||||
else
|
||||
{
|
||||
doc = e;
|
||||
e = doc.documentElement;
|
||||
}
|
||||
|
||||
// When no state was specified then return the current state.
|
||||
if (state == null)
|
||||
{
|
||||
// When fullscreen mode is not supported then return null
|
||||
if (!((/** @type {?Function} */ doc["exitFullscreen"])
|
||||
|| (/** @type {?Function} */ doc["webkitExitFullscreen"])
|
||||
|| (/** @type {?Function} */ doc["webkitCancelFullScreen"])
|
||||
|| (/** @type {?Function} */ doc["msExitFullscreen"])
|
||||
|| (/** @type {?Function} */ doc["mozCancelFullScreen"])))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check fullscreen state
|
||||
state = !!doc["fullscreenElement"]
|
||||
|| !!doc["msFullscreenElement"]
|
||||
|| !!doc["webkitIsFullScreen"]
|
||||
|| !!doc["mozFullScreen"];
|
||||
if (!state) return state;
|
||||
|
||||
// Return current fullscreen element or "true" if browser doesn't
|
||||
// support this
|
||||
return (/** @type {?Element} */ doc["fullscreenElement"])
|
||||
|| (/** @type {?Element} */ doc["webkitFullscreenElement"])
|
||||
|| (/** @type {?Element} */ doc["webkitCurrentFullScreenElement"])
|
||||
|| (/** @type {?Element} */ doc["msFullscreenElement"])
|
||||
|| (/** @type {?Element} */ doc["mozFullScreenElement"])
|
||||
|| state;
|
||||
}
|
||||
|
||||
// When state was specified then enter or exit fullscreen mode.
|
||||
if (state)
|
||||
{
|
||||
// Enter fullscreen
|
||||
func = (/** @type {?Function} */ e["requestFullscreen"])
|
||||
|| (/** @type {?Function} */ e["webkitRequestFullscreen"])
|
||||
|| (/** @type {?Function} */ e["webkitRequestFullScreen"])
|
||||
|| (/** @type {?Function} */ e["msRequestFullscreen"])
|
||||
|| (/** @type {?Function} */ e["mozRequestFullScreen"]);
|
||||
if (func)
|
||||
{
|
||||
func.call(e);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Exit fullscreen
|
||||
func = (/** @type {?Function} */ doc["exitFullscreen"])
|
||||
|| (/** @type {?Function} */ doc["webkitExitFullscreen"])
|
||||
|| (/** @type {?Function} */ doc["webkitCancelFullScreen"])
|
||||
|| (/** @type {?Function} */ doc["msExitFullscreen"])
|
||||
|| (/** @type {?Function} */ doc["mozCancelFullScreen"]);
|
||||
if (func) func.call(doc);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the fullscreen mode.
|
||||
*
|
||||
* @return {!jQuery}
|
||||
* The jQuery selection for chaining.
|
||||
* @this {jQuery}
|
||||
*/
|
||||
function toggleFullScreen()
|
||||
{
|
||||
return (/** @type {!jQuery} */ fullScreen.call(this,
|
||||
!fullScreen.call(this)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the browser-specific fullscreenchange event and triggers
|
||||
* a jquery event for it.
|
||||
*
|
||||
* @param {?Event} event
|
||||
* The fullscreenchange event.
|
||||
*/
|
||||
function fullScreenChangeHandler(event)
|
||||
{
|
||||
jQuery(document).trigger(new jQuery.Event("fullscreenchange"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the browser-specific fullscreenerror event and triggers
|
||||
* a jquery event for it.
|
||||
*
|
||||
* @param {?Event} event
|
||||
* The fullscreenerror event.
|
||||
*/
|
||||
function fullScreenErrorHandler(event)
|
||||
{
|
||||
jQuery(document).trigger(new jQuery.Event("fullscreenerror"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the fullscreenchange event handler.
|
||||
*/
|
||||
function installFullScreenHandlers()
|
||||
{
|
||||
var e, change, error;
|
||||
|
||||
// Determine event name
|
||||
e = document;
|
||||
if (e["webkitCancelFullScreen"])
|
||||
{
|
||||
change = "webkitfullscreenchange";
|
||||
error = "webkitfullscreenerror";
|
||||
}
|
||||
else if (e["msExitFullscreen"])
|
||||
{
|
||||
change = "MSFullscreenChange";
|
||||
error = "MSFullscreenError";
|
||||
}
|
||||
else if (e["mozCancelFullScreen"])
|
||||
{
|
||||
change = "mozfullscreenchange";
|
||||
error = "mozfullscreenerror";
|
||||
}
|
||||
else
|
||||
{
|
||||
change = "fullscreenchange";
|
||||
error = "fullscreenerror";
|
||||
}
|
||||
|
||||
// Install the event handlers
|
||||
jQuery(document).bind(change, fullScreenChangeHandler);
|
||||
jQuery(document).bind(error, fullScreenErrorHandler);
|
||||
}
|
||||
|
||||
jQuery.fn["fullScreen"] = fullScreen;
|
||||
jQuery.fn["toggleFullScreen"] = toggleFullScreen;
|
||||
installFullScreenHandlers();
|
||||
|
||||
})(jQuery);
|
||||
@ -1,165 +0,0 @@
|
||||
/*!
|
||||
* JavaScript Cookie v2.2.0
|
||||
* https://github.com/js-cookie/js-cookie
|
||||
*
|
||||
* Copyright 2006, 2015 Klaus Hartl & Fagner Brack
|
||||
* Released under the MIT license
|
||||
*/
|
||||
;(function (factory) {
|
||||
var registeredInModuleLoader = false;
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define(factory);
|
||||
registeredInModuleLoader = true;
|
||||
}
|
||||
if (typeof exports === 'object') {
|
||||
module.exports = factory();
|
||||
registeredInModuleLoader = true;
|
||||
}
|
||||
if (!registeredInModuleLoader) {
|
||||
var OldCookies = window.Cookies;
|
||||
var api = window.Cookies = factory();
|
||||
api.noConflict = function () {
|
||||
window.Cookies = OldCookies;
|
||||
return api;
|
||||
};
|
||||
}
|
||||
}(function () {
|
||||
function extend () {
|
||||
var i = 0;
|
||||
var result = {};
|
||||
for (; i < arguments.length; i++) {
|
||||
var attributes = arguments[ i ];
|
||||
for (var key in attributes) {
|
||||
result[key] = attributes[key];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function init (converter) {
|
||||
function api (key, value, attributes) {
|
||||
var result;
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Write
|
||||
|
||||
if (arguments.length > 1) {
|
||||
attributes = extend({
|
||||
path: '/'
|
||||
}, api.defaults, attributes);
|
||||
|
||||
if (typeof attributes.expires === 'number') {
|
||||
var expires = new Date();
|
||||
expires.setMilliseconds(expires.getMilliseconds() + attributes.expires * 864e+5);
|
||||
attributes.expires = expires;
|
||||
}
|
||||
|
||||
// We're using "expires" because "max-age" is not supported by IE
|
||||
attributes.expires = attributes.expires ? attributes.expires.toUTCString() : '';
|
||||
|
||||
try {
|
||||
result = JSON.stringify(value);
|
||||
if (/^[\{\[]/.test(result)) {
|
||||
value = result;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
if (!converter.write) {
|
||||
value = encodeURIComponent(String(value))
|
||||
.replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent);
|
||||
} else {
|
||||
value = converter.write(value, key);
|
||||
}
|
||||
|
||||
key = encodeURIComponent(String(key));
|
||||
key = key.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent);
|
||||
key = key.replace(/[\(\)]/g, escape);
|
||||
|
||||
var stringifiedAttributes = '';
|
||||
|
||||
for (var attributeName in attributes) {
|
||||
if (!attributes[attributeName]) {
|
||||
continue;
|
||||
}
|
||||
stringifiedAttributes += '; ' + attributeName;
|
||||
if (attributes[attributeName] === true) {
|
||||
continue;
|
||||
}
|
||||
stringifiedAttributes += '=' + attributes[attributeName];
|
||||
}
|
||||
return (document.cookie = key + '=' + value + stringifiedAttributes);
|
||||
}
|
||||
|
||||
// Read
|
||||
|
||||
if (!key) {
|
||||
result = {};
|
||||
}
|
||||
|
||||
// To prevent the for loop in the first place assign an empty array
|
||||
// in case there are no cookies at all. Also prevents odd result when
|
||||
// calling "get()"
|
||||
var cookies = document.cookie ? document.cookie.split('; ') : [];
|
||||
var rdecode = /(%[0-9A-Z]{2})+/g;
|
||||
var i = 0;
|
||||
|
||||
for (; i < cookies.length; i++) {
|
||||
var parts = cookies[i].split('=');
|
||||
var cookie = parts.slice(1).join('=');
|
||||
|
||||
if (!this.json && cookie.charAt(0) === '"') {
|
||||
cookie = cookie.slice(1, -1);
|
||||
}
|
||||
|
||||
try {
|
||||
var name = parts[0].replace(rdecode, decodeURIComponent);
|
||||
cookie = converter.read ?
|
||||
converter.read(cookie, name) : converter(cookie, name) ||
|
||||
cookie.replace(rdecode, decodeURIComponent);
|
||||
|
||||
if (this.json) {
|
||||
try {
|
||||
cookie = JSON.parse(cookie);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (key === name) {
|
||||
result = cookie;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
result[name] = cookie;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
api.set = api;
|
||||
api.get = function (key) {
|
||||
return api.call(api, key);
|
||||
};
|
||||
api.getJSON = function () {
|
||||
return api.apply({
|
||||
json: true
|
||||
}, [].slice.call(arguments));
|
||||
};
|
||||
api.defaults = {};
|
||||
|
||||
api.remove = function (key, attributes) {
|
||||
api(key, '', extend(attributes, {
|
||||
expires: -1
|
||||
}));
|
||||
};
|
||||
|
||||
api.withConverter = init;
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
return init(function () {});
|
||||
}));
|
||||
@ -1,227 +0,0 @@
|
||||
/*! svg.draggable.js - v2.2.1 - 2016-08-25
|
||||
* https://github.com/wout/svg.draggable.js
|
||||
* Copyright (c) 2016 Wout Fierens; Licensed MIT */
|
||||
;(function() {
|
||||
|
||||
// creates handler, saves it
|
||||
function DragHandler(el){
|
||||
el.remember('_draggable', this)
|
||||
this.el = el
|
||||
}
|
||||
|
||||
|
||||
// Sets new parameter, starts dragging
|
||||
DragHandler.prototype.init = function(constraint, val){
|
||||
var _this = this
|
||||
this.constraint = constraint
|
||||
this.value = val
|
||||
this.el.on('mousedown.drag', function(e){ _this.start(e) })
|
||||
this.el.on('touchstart.drag', function(e){ _this.start(e) })
|
||||
}
|
||||
|
||||
// transforms one point from screen to user coords
|
||||
DragHandler.prototype.transformPoint = function(event, offset){
|
||||
event = event || window.event
|
||||
var touches = event.changedTouches && event.changedTouches[0] || event
|
||||
this.p.x = touches.pageX - (offset || 0)
|
||||
this.p.y = touches.pageY
|
||||
return this.p.matrixTransform(this.m)
|
||||
}
|
||||
|
||||
// gets elements bounding box with special handling of groups, nested and use
|
||||
DragHandler.prototype.getBBox = function(){
|
||||
|
||||
var box = this.el.bbox()
|
||||
|
||||
if(this.el instanceof SVG.Nested) box = this.el.rbox()
|
||||
|
||||
if (this.el instanceof SVG.G || this.el instanceof SVG.Use || this.el instanceof SVG.Nested) {
|
||||
box.x = this.el.x()
|
||||
box.y = this.el.y()
|
||||
}
|
||||
|
||||
return box
|
||||
}
|
||||
|
||||
// start dragging
|
||||
DragHandler.prototype.start = function(e){
|
||||
|
||||
// check for left button
|
||||
if(e.type == 'click'|| e.type == 'mousedown' || e.type == 'mousemove'){
|
||||
if((e.which || e.buttons) != 1){
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var _this = this
|
||||
|
||||
// fire beforedrag event
|
||||
this.el.fire('beforedrag', { event: e, handler: this })
|
||||
|
||||
// search for parent on the fly to make sure we can call
|
||||
// draggable() even when element is not in the dom currently
|
||||
this.parent = this.parent || this.el.parent(SVG.Nested) || this.el.parent(SVG.Doc)
|
||||
this.p = this.parent.node.createSVGPoint()
|
||||
|
||||
// save current transformation matrix
|
||||
this.m = this.el.node.getScreenCTM().inverse()
|
||||
|
||||
var box = this.getBBox()
|
||||
|
||||
var anchorOffset;
|
||||
|
||||
// fix text-anchor in text-element (#37)
|
||||
if(this.el instanceof SVG.Text){
|
||||
anchorOffset = this.el.node.getComputedTextLength();
|
||||
|
||||
switch(this.el.attr('text-anchor')){
|
||||
case 'middle':
|
||||
anchorOffset /= 2;
|
||||
break
|
||||
case 'start':
|
||||
anchorOffset = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.startPoints = {
|
||||
// We take absolute coordinates since we are just using a delta here
|
||||
point: this.transformPoint(e, anchorOffset),
|
||||
box: box,
|
||||
transform: this.el.transform()
|
||||
}
|
||||
|
||||
// add drag and end events to window
|
||||
SVG.on(window, 'mousemove.drag', function(e){ _this.drag(e) })
|
||||
SVG.on(window, 'touchmove.drag', function(e){ _this.drag(e) })
|
||||
SVG.on(window, 'mouseup.drag', function(e){ _this.end(e) })
|
||||
SVG.on(window, 'touchend.drag', function(e){ _this.end(e) })
|
||||
|
||||
// fire dragstart event
|
||||
this.el.fire('dragstart', {event: e, p: this.startPoints.point, m: this.m, handler: this})
|
||||
|
||||
// prevent browser drag behavior
|
||||
e.preventDefault()
|
||||
|
||||
// prevent propagation to a parent that might also have dragging enabled
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// while dragging
|
||||
DragHandler.prototype.drag = function(e){
|
||||
|
||||
var box = this.getBBox()
|
||||
, p = this.transformPoint(e)
|
||||
, x = this.startPoints.box.x + p.x - this.startPoints.point.x
|
||||
, y = this.startPoints.box.y + p.y - this.startPoints.point.y
|
||||
, c = this.constraint
|
||||
, gx = p.x - this.startPoints.point.x
|
||||
, gy = p.y - this.startPoints.point.y
|
||||
|
||||
var event = new CustomEvent('dragmove', {
|
||||
detail: {
|
||||
event: e
|
||||
, p: p
|
||||
, m: this.m
|
||||
, handler: this
|
||||
}
|
||||
, cancelable: true
|
||||
})
|
||||
|
||||
this.el.fire(event)
|
||||
|
||||
if(event.defaultPrevented) return p
|
||||
|
||||
// move the element to its new position, if possible by constraint
|
||||
if (typeof c == 'function') {
|
||||
|
||||
var coord = c.call(this.el, x, y, this.m)
|
||||
|
||||
// bool, just show us if movement is allowed or not
|
||||
if (typeof coord == 'boolean') {
|
||||
coord = {
|
||||
x: coord,
|
||||
y: coord
|
||||
}
|
||||
}
|
||||
|
||||
// if true, we just move. If !false its a number and we move it there
|
||||
if (coord.x === true) {
|
||||
this.el.x(x)
|
||||
} else if (coord.x !== false) {
|
||||
this.el.x(coord.x)
|
||||
}
|
||||
|
||||
if (coord.y === true) {
|
||||
this.el.y(y)
|
||||
} else if (coord.y !== false) {
|
||||
this.el.y(coord.y)
|
||||
}
|
||||
|
||||
} else if (typeof c == 'object') {
|
||||
|
||||
// keep element within constrained box
|
||||
if (c.minX != null && x < c.minX)
|
||||
x = c.minX
|
||||
else if (c.maxX != null && x > c.maxX - box.width){
|
||||
x = c.maxX - box.width
|
||||
}if (c.minY != null && y < c.minY)
|
||||
y = c.minY
|
||||
else if (c.maxY != null && y > c.maxY - box.height)
|
||||
y = c.maxY - box.height
|
||||
|
||||
if(this.el instanceof SVG.G)
|
||||
this.el.matrix(this.startPoints.transform).transform({x:gx, y: gy}, true)
|
||||
else
|
||||
this.el.move(x, y)
|
||||
}
|
||||
|
||||
// so we can use it in the end-method, too
|
||||
return p
|
||||
}
|
||||
|
||||
DragHandler.prototype.end = function(e){
|
||||
|
||||
// final drag
|
||||
var p = this.drag(e);
|
||||
|
||||
// fire dragend event
|
||||
this.el.fire('dragend', { event: e, p: p, m: this.m, handler: this })
|
||||
|
||||
// unbind events
|
||||
SVG.off(window, 'mousemove.drag')
|
||||
SVG.off(window, 'touchmove.drag')
|
||||
SVG.off(window, 'mouseup.drag')
|
||||
SVG.off(window, 'touchend.drag')
|
||||
|
||||
}
|
||||
|
||||
SVG.extend(SVG.Element, {
|
||||
// Make element draggable
|
||||
// Constraint might be an object (as described in readme.md) or a function in the form "function (x, y)" that gets called before every move.
|
||||
// The function can return a boolean or an object of the form {x, y}, to which the element will be moved. "False" skips moving, true moves to raw x, y.
|
||||
draggable: function(value, constraint) {
|
||||
|
||||
// Check the parameters and reassign if needed
|
||||
if (typeof value == 'function' || typeof value == 'object') {
|
||||
constraint = value
|
||||
value = true
|
||||
}
|
||||
|
||||
var dragHandler = this.remember('_draggable') || new DragHandler(this)
|
||||
|
||||
// When no parameter is given, value is true
|
||||
value = typeof value === 'undefined' ? true : value
|
||||
|
||||
if(value) dragHandler.init(constraint || {}, value)
|
||||
else {
|
||||
this.off('mousedown.drag')
|
||||
this.off('touchstart.drag')
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}).call(this);
|
||||
@ -1,442 +0,0 @@
|
||||
/*! svg.draw.js - v2.0.3 - 2017-06-19
|
||||
* https://github.com/svgdotjs/svg.draw.js
|
||||
* Copyright (c) 2017 Ulrich-Matthias Schäfer; Licensed MIT */
|
||||
|
||||
;(function () {
|
||||
// Our Object which manages drawing
|
||||
function PaintHandler(el, event, options) {
|
||||
|
||||
this.el = el;
|
||||
el.remember('_paintHandler', this);
|
||||
|
||||
var _this = this,
|
||||
plugin = this.getPlugin();
|
||||
|
||||
this.parent = el.parent(SVG.Nested) || el.parent(SVG.Doc);
|
||||
this.p = this.parent.node.createSVGPoint(); // Helping point for coord transformation
|
||||
this.m = null; // transformation matrix. We get it when drawing starts
|
||||
this.startPoint = null;
|
||||
this.lastUpdateCall = null;
|
||||
this.options = {};
|
||||
|
||||
// Merge options and defaults
|
||||
for (var i in this.el.draw.defaults) {
|
||||
this.options[i] = this.el.draw.defaults[i];
|
||||
if (typeof options[i] !== 'undefined') {
|
||||
this.options[i] = options[i];
|
||||
}
|
||||
}
|
||||
|
||||
if(plugin.point) {
|
||||
plugin['pointPlugin'] = plugin.point;
|
||||
delete plugin.point;
|
||||
}
|
||||
|
||||
// Import all methods from plugin into object
|
||||
for (var i in plugin){
|
||||
this[i] = plugin[i];
|
||||
}
|
||||
|
||||
// When we got an event, we use this for start, otherwise we use the click-event as default
|
||||
if (!event) {
|
||||
this.parent.on('click.draw', function (e) {
|
||||
_this.start(e);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PaintHandler.prototype.transformPoint = function(x, y){
|
||||
|
||||
this.p.x = x - (this.offset.x - window.pageXOffset);
|
||||
this.p.y = y - (this.offset.y - window.pageYOffset);
|
||||
|
||||
return this.p.matrixTransform(this.m);
|
||||
|
||||
}
|
||||
|
||||
PaintHandler.prototype.start = function (event) {
|
||||
|
||||
var _this = this;
|
||||
|
||||
// get the current transform matrix from screen to element (offset corrected)
|
||||
this.m = this.el.node.getScreenCTM().inverse();
|
||||
|
||||
// we save the current scrolling-offset here
|
||||
this.offset = { x: window.pageXOffset, y: window.pageYOffset };
|
||||
|
||||
// we want to snap in screen-coords, so we have to scale the snapToGrid accordingly
|
||||
this.options.snapToGrid *= Math.sqrt(this.m.a * this.m.a + this.m.b * this.m.b)
|
||||
|
||||
// save the startpoint
|
||||
this.startPoint = this.snapToGrid(this.transformPoint(event.clientX, event.clientY));
|
||||
|
||||
// the plugin may do some initial work
|
||||
if(this.init){ this.init(event); }
|
||||
|
||||
// Fire our `drawstart`-event. We send the offset-corrected cursor-position along
|
||||
this.el.fire('drawstart', {event:event, p:this.p, m:this.m});
|
||||
|
||||
// We need to bind the update-function to the mousemove event to keep track of the cursor
|
||||
SVG.on(window, 'mousemove.draw', function (e) {
|
||||
_this.update(e);
|
||||
});
|
||||
|
||||
// Every consecutive call to start should map to point now
|
||||
this.start = this.point;
|
||||
|
||||
|
||||
};
|
||||
|
||||
// This function draws a point if the element is a polyline or polygon
|
||||
// Otherwise it will just stop drawing the shape cause we are done
|
||||
PaintHandler.prototype.point = function (event) {
|
||||
if (this.point != this.start) return this.start(event);
|
||||
|
||||
if (this.pointPlugin) {
|
||||
return this.pointPlugin(event);
|
||||
}
|
||||
|
||||
// If this function is not overwritten we just call stop
|
||||
this.stop(event);
|
||||
};
|
||||
|
||||
|
||||
// The stop-function does the cleanup work
|
||||
PaintHandler.prototype.stop = function (event) {
|
||||
if (event) {
|
||||
this.update(event);
|
||||
}
|
||||
|
||||
// Plugin may want to clean something
|
||||
if(this.clean){ this.clean(); }
|
||||
|
||||
// Unbind from all events
|
||||
SVG.off(window, 'mousemove.draw');
|
||||
this.parent.off('click.draw');
|
||||
|
||||
// remove Refernce to PaintHandler
|
||||
this.el.forget('_paintHandler');
|
||||
|
||||
// overwrite draw-function since we never need it again for this element
|
||||
this.el.draw = function () {
|
||||
};
|
||||
|
||||
// Fire the `drawstop`-event
|
||||
this.el.fire('drawstop');
|
||||
};
|
||||
|
||||
// Updates the element while moving the cursor
|
||||
PaintHandler.prototype.update = function (event) {
|
||||
|
||||
if(!event && this.lastUpdateCall){
|
||||
event = this.lastUpdateCall;
|
||||
}
|
||||
|
||||
this.lastUpdateCall = event;
|
||||
|
||||
// Call the calc-function which calculates the new position and size
|
||||
this.calc(event);
|
||||
|
||||
// Fire the `drawupdate`-event
|
||||
this.el.fire('drawupdate', {event:event, p:this.p, m:this.m});
|
||||
};
|
||||
|
||||
// Called from outside. Finishs a poly-element
|
||||
PaintHandler.prototype.done = function () {
|
||||
this.calc();
|
||||
this.stop();
|
||||
|
||||
this.el.fire('drawdone');
|
||||
};
|
||||
|
||||
// Called from outside. Cancels a poly-element
|
||||
PaintHandler.prototype.cancel = function () {
|
||||
// stop drawing and remove the element
|
||||
this.stop();
|
||||
this.el.remove();
|
||||
|
||||
this.el.fire('drawcancel');
|
||||
};
|
||||
|
||||
// Calculate the corrected position when using `snapToGrid`
|
||||
PaintHandler.prototype.snapToGrid = function (draw) {
|
||||
|
||||
var temp = null;
|
||||
|
||||
// An array was given. Loop through every element
|
||||
if (draw.length) {
|
||||
temp = [draw[0] % this.options.snapToGrid, draw[1] % this.options.snapToGrid];
|
||||
draw[0] -= temp[0] < this.options.snapToGrid / 2 ? temp[0] : temp[0] - this.options.snapToGrid;
|
||||
draw[1] -= temp[1] < this.options.snapToGrid / 2 ? temp[1] : temp[1] - this.options.snapToGrid;
|
||||
return draw;
|
||||
}
|
||||
|
||||
// Properties of element were given. Snap them all
|
||||
for (var i in draw) {
|
||||
temp = draw[i] % this.options.snapToGrid;
|
||||
draw[i] -= (temp < this.options.snapToGrid / 2 ? temp : temp - this.options.snapToGrid) + (temp < 0 ? this.options.snapToGrid : 0);
|
||||
}
|
||||
|
||||
return draw;
|
||||
};
|
||||
|
||||
PaintHandler.prototype.param = function (key, value) {
|
||||
this.options[key] = value === null ? this.el.draw.defaults[key] : value;
|
||||
this.update();
|
||||
};
|
||||
|
||||
// Returns the plugin
|
||||
PaintHandler.prototype.getPlugin = function () {
|
||||
return this.el.draw.plugins[this.el.type];
|
||||
};
|
||||
|
||||
SVG.extend(SVG.Element, {
|
||||
// Draw element with mouse
|
||||
draw: function (event, options, value) {
|
||||
|
||||
// sort the parameters
|
||||
if (!(event instanceof Event || typeof event === 'string')) {
|
||||
options = event;
|
||||
event = null;
|
||||
}
|
||||
|
||||
// get the old Handler or create a new one from event and options
|
||||
var paintHandler = this.remember('_paintHandler') || new PaintHandler(this, event, options || {});
|
||||
|
||||
// When we got an event we have to start/continue drawing
|
||||
if (event instanceof Event) {
|
||||
paintHandler['start'](event);
|
||||
}
|
||||
|
||||
// if event is located in our PaintHandler we handle it as method
|
||||
if (paintHandler[event]) {
|
||||
paintHandler[event](options, value);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Default values. Can be changed for the whole project if needed
|
||||
SVG.Element.prototype.draw.defaults = {
|
||||
snapToGrid: 1 // Snaps to a grid of `snapToGrid` px
|
||||
};
|
||||
|
||||
SVG.Element.prototype.draw.extend = function(name, obj){
|
||||
|
||||
var plugins = {};
|
||||
if(typeof name === 'string'){
|
||||
plugins[name] = obj;
|
||||
}else{
|
||||
plugins = name;
|
||||
}
|
||||
|
||||
for(var shapes in plugins){
|
||||
var shapesArr = shapes.trim().split(/\s+/);
|
||||
|
||||
for(var i in shapesArr){
|
||||
SVG.Element.prototype.draw.plugins[shapesArr[i]] = plugins[shapes];
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// Container for all types not specified here
|
||||
SVG.Element.prototype.draw.plugins = {};
|
||||
|
||||
SVG.Element.prototype.draw.extend('rect image', {
|
||||
|
||||
init:function(e){
|
||||
|
||||
var p = this.startPoint;
|
||||
|
||||
this.el.attr({ x: p.x, y: p.y, height: 0, width: 0 });
|
||||
},
|
||||
|
||||
calc:function (e) {
|
||||
|
||||
var rect = {
|
||||
x: this.startPoint.x,
|
||||
y: this.startPoint.y
|
||||
}, p = this.transformPoint(e.clientX, e.clientY);
|
||||
|
||||
rect.width = p.x - rect.x;
|
||||
rect.height = p.y - rect.y;
|
||||
|
||||
// Snap the params to the grid we specified
|
||||
this.snapToGrid(rect);
|
||||
|
||||
// When width is less than zero, we have to draw to the left
|
||||
// which means we have to move the start-point to the left
|
||||
if (rect.width < 0) {
|
||||
rect.x = rect.x + rect.width;
|
||||
rect.width = -rect.width;
|
||||
}
|
||||
|
||||
// ...same with height
|
||||
if (rect.height < 0) {
|
||||
rect.y = rect.y + rect.height;
|
||||
rect.height = -rect.height;
|
||||
}
|
||||
|
||||
// draw the element
|
||||
this.el.attr(rect);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
SVG.Element.prototype.draw.extend('line polyline polygon', {
|
||||
|
||||
init:function(e){
|
||||
// When we draw a polygon, we immediately need 2 points.
|
||||
// One start-point and one point at the mouse-position
|
||||
|
||||
this.set = new SVG.Set();
|
||||
|
||||
var p = this.startPoint,
|
||||
arr = [
|
||||
[p.x, p.y],
|
||||
[p.x, p.y]
|
||||
];
|
||||
|
||||
this.el.plot(arr);
|
||||
|
||||
// We draw little circles around each point
|
||||
// This is absolutely not needed and maybe removed in a later release
|
||||
this.drawCircles();
|
||||
|
||||
},
|
||||
|
||||
|
||||
// The calc-function sets the position of the last point to the mouse-position (with offset ofc)
|
||||
calc:function (e) {
|
||||
var arr = this.el.array().valueOf();
|
||||
arr.pop();
|
||||
|
||||
if (e) {
|
||||
var p = this.transformPoint(e.clientX, e.clientY);
|
||||
arr.push(this.snapToGrid([p.x, p.y]));
|
||||
}
|
||||
|
||||
this.el.plot(arr);
|
||||
|
||||
},
|
||||
|
||||
point:function(e){
|
||||
|
||||
if (this.el.type.indexOf('poly') > -1) {
|
||||
// Add the new Point to the point-array
|
||||
var p = this.transformPoint(e.clientX, e.clientY),
|
||||
arr = this.el.array().valueOf();
|
||||
|
||||
arr.push(this.snapToGrid([p.x, p.y]));
|
||||
|
||||
this.el.plot(arr);
|
||||
this.drawCircles();
|
||||
|
||||
// Fire the `drawpoint`-event, which holds the coords of the new Point
|
||||
this.el.fire('drawpoint', {event:e, p:{x:p.x, y:p.y}, m:this.m});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// We are done, if the element is no polyline or polygon
|
||||
this.stop(e);
|
||||
|
||||
},
|
||||
|
||||
clean:function(){
|
||||
|
||||
// Remove all circles
|
||||
this.set.each(function () {
|
||||
this.remove();
|
||||
});
|
||||
|
||||
this.set.clear();
|
||||
|
||||
delete this.set;
|
||||
|
||||
},
|
||||
|
||||
drawCircles:function () {
|
||||
var array = this.el.array().valueOf()
|
||||
|
||||
this.set.each(function () {
|
||||
this.remove();
|
||||
});
|
||||
|
||||
this.set.clear();
|
||||
|
||||
for (var i = 0; i < array.length; ++i) {
|
||||
|
||||
this.p.x = array[i][0]
|
||||
this.p.y = array[i][1]
|
||||
|
||||
var p = this.p.matrixTransform(this.parent.node.getScreenCTM().inverse().multiply(this.el.node.getScreenCTM()));
|
||||
|
||||
this.set.add(this.parent.circle(5).stroke({width: 1}).fill('#ccc').center(p.x, p.y));
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
SVG.Element.prototype.draw.extend('circle', {
|
||||
|
||||
init:function(e){
|
||||
|
||||
var p = this.startPoint;
|
||||
|
||||
this.el.attr({ cx: p.x, cy: p.y, r: 1 });
|
||||
},
|
||||
|
||||
// We determine the radius by the cursor position
|
||||
calc:function (e) {
|
||||
|
||||
var p = this.transformPoint(e.clientX, e.clientY),
|
||||
circle = {
|
||||
cx: this.startPoint.x,
|
||||
cy: this.startPoint.y,
|
||||
|
||||
// calculating the radius
|
||||
r: Math.sqrt(
|
||||
(p.x - this.startPoint.x) * (p.x - this.startPoint.x) +
|
||||
(p.y - this.startPoint.y) * (p.y - this.startPoint.y)
|
||||
)
|
||||
};
|
||||
|
||||
this.snapToGrid(circle);
|
||||
this.el.attr(circle);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
SVG.Element.prototype.draw.extend('ellipse', {
|
||||
|
||||
init:function(e){
|
||||
// We start with a circle with radius 1 at the position of the cursor
|
||||
var p = this.startPoint;
|
||||
|
||||
this.el.attr({ cx: p.x, cy: p.y, rx: 1, ry: 1 });
|
||||
|
||||
},
|
||||
|
||||
calc:function (e) {
|
||||
var p = this.transformPoint(e.clientX, e.clientY);
|
||||
|
||||
var ellipse = {
|
||||
cx: this.startPoint.x,
|
||||
cy: this.startPoint.y,
|
||||
rx: Math.abs(p.x - this.startPoint.x),
|
||||
ry: Math.abs(p.y - this.startPoint.y)
|
||||
};
|
||||
|
||||
this.snapToGrid(ellipse);
|
||||
this.el.attr(ellipse);
|
||||
}
|
||||
|
||||
});
|
||||
}).call(this);
|
||||
@ -1,417 +0,0 @@
|
||||
/*!
|
||||
* svg.select.js - An extension of svg.js which allows to select elements with mouse
|
||||
* @version 3.0.0
|
||||
* https://github.com/svgdotjs/svg.select.js
|
||||
*
|
||||
* @copyright Ulrich-Matthias Schäfer
|
||||
* @license MIT
|
||||
*/;
|
||||
;(function() {
|
||||
"use strict";
|
||||
|
||||
function SelectHandler(el) {
|
||||
|
||||
this.el = el;
|
||||
el.remember('_selectHandler', this);
|
||||
this.pointSelection = {isSelected: false};
|
||||
this.rectSelection = {isSelected: false};
|
||||
|
||||
// helper list with position settings of each type of point
|
||||
this.pointsList = {
|
||||
lt: [ 0, 0 ],
|
||||
rt: [ 'width', 0 ],
|
||||
rb: [ 'width', 'height' ],
|
||||
lb: [ 0, 'height' ],
|
||||
t: [ 'width / 2', 0 ],
|
||||
r: [ 'width', 'height / 2' ],
|
||||
b: [ 'width / 2', 'height' ],
|
||||
l: [ 0, 'height / 2' ]
|
||||
};
|
||||
|
||||
// helper function to evaluate point coordinates based on settings above and an object (bbox in our case)
|
||||
this.pointCoord = function (setting, object) {
|
||||
return typeof setting !== 'string' ? setting : eval('object.' + setting)
|
||||
}
|
||||
|
||||
this.pointCoords = function (point, object) {
|
||||
var settings = this.pointsList[point];
|
||||
|
||||
return {
|
||||
x: this.pointCoord(settings[0], object),
|
||||
y: this.pointCoord(settings[1], object)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SelectHandler.prototype.init = function (value, options) {
|
||||
|
||||
var bbox = this.el.bbox();
|
||||
this.options = {};
|
||||
|
||||
// store defaults list of points in order to verify users config
|
||||
var points = this.el.selectize.defaults.points;
|
||||
|
||||
// Merging the defaults and the options-object together
|
||||
for (var i in this.el.selectize.defaults) {
|
||||
this.options[i] = this.el.selectize.defaults[i];
|
||||
if (options[i] !== undefined) {
|
||||
this.options[i] = options[i];
|
||||
}
|
||||
}
|
||||
|
||||
// prepare & validate list of points to be added (or excluded)
|
||||
var pointsLists = ['points', 'pointsExclude'];
|
||||
|
||||
for (var i in pointsLists) {
|
||||
var option = this.options[pointsLists[i]];
|
||||
|
||||
if (typeof option === 'string') {
|
||||
if (option.length > 0) {
|
||||
// if set as comma separated string list => convert it into an array
|
||||
option = option.split(/\s*,\s*/i);
|
||||
} else {
|
||||
option = [];
|
||||
}
|
||||
} else if (typeof option === 'boolean' && pointsLists[i] === 'points') {
|
||||
// this is not needed, but let's have it for legacy support
|
||||
option = option ? points : [];
|
||||
}
|
||||
|
||||
this.options[pointsLists[i]] = option;
|
||||
}
|
||||
|
||||
// intersect correct all points options with users config (exclude unwanted points)
|
||||
// ES5 -> NO arrow functions nor Array.includes()
|
||||
this.options.points = [ points, this.options.points ].reduce(
|
||||
function (a, b) {
|
||||
return a.filter(
|
||||
function (c) {
|
||||
return b.indexOf(c) > -1;
|
||||
}
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
// exclude pointsExclude, if wanted
|
||||
this.options.points = [ this.options.points, this.options.pointsExclude ].reduce(
|
||||
function (a, b) {
|
||||
return a.filter(
|
||||
function (c) {
|
||||
return b.indexOf(c) < 0;
|
||||
}
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
this.parent = this.el.parent();
|
||||
this.nested = (this.nested || this.parent.group());
|
||||
this.nested.matrix(new SVG.Matrix(this.el).translate(bbox.x, bbox.y));
|
||||
|
||||
// When deepSelect is enabled and the element is a line/polyline/polygon, draw only points for moving
|
||||
if (this.options.deepSelect && ['line', 'polyline', 'polygon'].indexOf(this.el.type) !== -1) {
|
||||
this.selectPoints(value);
|
||||
} else {
|
||||
this.selectRect(value);
|
||||
}
|
||||
|
||||
this.observe();
|
||||
this.cleanup();
|
||||
|
||||
};
|
||||
|
||||
SelectHandler.prototype.selectPoints = function (value) {
|
||||
|
||||
this.pointSelection.isSelected = value;
|
||||
|
||||
// When set is already there we dont have to create one
|
||||
if (this.pointSelection.set) {
|
||||
return this;
|
||||
}
|
||||
|
||||
// Create our set of elements
|
||||
this.pointSelection.set = this.parent.set();
|
||||
// draw the points and mark the element as selected
|
||||
this.drawPoints();
|
||||
|
||||
return this;
|
||||
|
||||
};
|
||||
|
||||
// create the point-array which contains the 2 points of a line or simply the points-array of polyline/polygon
|
||||
SelectHandler.prototype.getPointArray = function () {
|
||||
var bbox = this.el.bbox();
|
||||
|
||||
return this.el.array().valueOf().map(function (el) {
|
||||
return [el[0] - bbox.x, el[1] - bbox.y];
|
||||
});
|
||||
};
|
||||
|
||||
// Draws a points
|
||||
SelectHandler.prototype.drawPoints = function () {
|
||||
|
||||
var _this = this, array = this.getPointArray();
|
||||
|
||||
// go through the array of points
|
||||
for (var i = 0, len = array.length; i < len; ++i) {
|
||||
|
||||
var curriedEvent = (function (k) {
|
||||
return function (ev) {
|
||||
ev = ev || window.event;
|
||||
ev.preventDefault ? ev.preventDefault() : ev.returnValue = false;
|
||||
ev.stopPropagation();
|
||||
|
||||
var x = ev.pageX || ev.touches[0].pageX;
|
||||
var y = ev.pageY || ev.touches[0].pageY;
|
||||
_this.el.fire('point', {x: x, y: y, i: k, event: ev});
|
||||
};
|
||||
})(i);
|
||||
|
||||
// add every point to the set
|
||||
// add css-classes and a touchstart-event which fires our event for moving points
|
||||
var point = this.drawPoint(array[i][0], array[i][1])
|
||||
.addClass(this.options.classPoints)
|
||||
.addClass(this.options.classPoints + '_point')
|
||||
.on('touchstart', curriedEvent)
|
||||
.on('mousedown', curriedEvent)
|
||||
this.pointSelection.set.add(point);
|
||||
}
|
||||
};
|
||||
|
||||
// The function to draw single point
|
||||
SelectHandler.prototype.drawPoint = function (cx, cy) {
|
||||
var pointType = this.options.pointType;
|
||||
|
||||
switch (pointType) {
|
||||
case 'circle':
|
||||
return this.drawCircle(cx, cy);
|
||||
case 'rect':
|
||||
return this.drawRect(cx, cy);
|
||||
default:
|
||||
if (typeof pointType === 'function') {
|
||||
return pointType.call(this, cx, cy);
|
||||
}
|
||||
|
||||
throw new Error('Unknown ' + pointType + ' point type!');
|
||||
}
|
||||
};
|
||||
|
||||
// The function to draw the circle point
|
||||
SelectHandler.prototype.drawCircle = function (cx, cy) {
|
||||
return this.nested.circle(this.options.pointSize)
|
||||
.center(cx, cy);
|
||||
};
|
||||
|
||||
// The function to draw the rect point
|
||||
SelectHandler.prototype.drawRect = function (cx, cy) {
|
||||
return this.nested.rect(this.options.pointSize, this.options.pointSize)
|
||||
.center(cx, cy);
|
||||
};
|
||||
|
||||
// every time a point is moved, we have to update the positions of our point
|
||||
SelectHandler.prototype.updatePointSelection = function () {
|
||||
var array = this.getPointArray();
|
||||
|
||||
this.pointSelection.set.each(function (i) {
|
||||
if (this.cx() === array[i][0] && this.cy() === array[i][1]) {
|
||||
return;
|
||||
}
|
||||
this.center(array[i][0], array[i][1]);
|
||||
});
|
||||
};
|
||||
|
||||
SelectHandler.prototype.updateRectSelection = function () {
|
||||
var _this = this, bbox = this.el.bbox();
|
||||
|
||||
this.rectSelection.set.get(0).attr({
|
||||
width: bbox.width,
|
||||
height: bbox.height
|
||||
});
|
||||
|
||||
// set.get(1) is always in the upper left corner. no need to move it
|
||||
if (this.options.points.length) {
|
||||
this.options.points.map(function (point, index) {
|
||||
var coords = _this.pointCoords(point, bbox);
|
||||
|
||||
_this.rectSelection.set.get(index + 1).center(coords.x, coords.y);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.options.rotationPoint) {
|
||||
var length = this.rectSelection.set.length();
|
||||
|
||||
this.rectSelection.set.get(length - 1).center(bbox.width / 2, 20);
|
||||
}
|
||||
};
|
||||
|
||||
SelectHandler.prototype.selectRect = function (value) {
|
||||
|
||||
var _this = this, bbox = this.el.bbox();
|
||||
|
||||
this.rectSelection.isSelected = value;
|
||||
|
||||
// when set is already p
|
||||
this.rectSelection.set = this.rectSelection.set || this.parent.set();
|
||||
|
||||
// helperFunction to create a mouse-down function which triggers the event specified in `eventName`
|
||||
function getMoseDownFunc(eventName) {
|
||||
return function (ev) {
|
||||
ev = ev || window.event;
|
||||
ev.preventDefault ? ev.preventDefault() : ev.returnValue = false;
|
||||
ev.stopPropagation();
|
||||
|
||||
var x = ev.pageX || ev.touches[0].pageX;
|
||||
var y = ev.pageY || ev.touches[0].pageY;
|
||||
_this.el.fire(eventName, {x: x, y: y, event: ev});
|
||||
};
|
||||
}
|
||||
|
||||
// create the selection-rectangle and add the css-class
|
||||
if (!this.rectSelection.set.get(0)) {
|
||||
this.rectSelection.set.add(this.nested.rect(bbox.width, bbox.height).addClass(this.options.classRect));
|
||||
}
|
||||
|
||||
// Draw Points at the edges, if enabled
|
||||
if (this.options.points.length && this.rectSelection.set.length() < 2) {
|
||||
var ename ="touchstart", mname = "mousedown";
|
||||
|
||||
this.options.points.map(function (point, index) {
|
||||
var coords = _this.pointCoords(point, bbox);
|
||||
|
||||
var pointElement = _this.drawPoint(coords.x, coords.y)
|
||||
.attr('class', _this.options.classPoints + '_' + point)
|
||||
.on(mname, getMoseDownFunc(point))
|
||||
.on(ename, getMoseDownFunc(point));
|
||||
_this.rectSelection.set.add(pointElement);
|
||||
});
|
||||
|
||||
this.rectSelection.set.each(function () {
|
||||
this.addClass(_this.options.classPoints);
|
||||
});
|
||||
}
|
||||
|
||||
// draw rotationPint, if enabled
|
||||
if (this.options.rotationPoint && ((this.options.points && !this.rectSelection.set.get(9)) || (!this.options.points && !this.rectSelection.set.get(1)))) {
|
||||
|
||||
var curriedEvent = function (ev) {
|
||||
ev = ev || window.event;
|
||||
ev.preventDefault ? ev.preventDefault() : ev.returnValue = false;
|
||||
ev.stopPropagation();
|
||||
|
||||
var x = ev.pageX || ev.touches[0].pageX;
|
||||
var y = ev.pageY || ev.touches[0].pageY;
|
||||
_this.el.fire('rot', {x: x, y: y, event: ev});
|
||||
};
|
||||
|
||||
var pointElement = this.drawPoint(bbox.width / 2, 20)
|
||||
.attr('class', this.options.classPoints + '_rot')
|
||||
.on("touchstart", curriedEvent)
|
||||
.on("mousedown", curriedEvent);
|
||||
this.rectSelection.set.add(pointElement);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
SelectHandler.prototype.handler = function () {
|
||||
|
||||
var bbox = this.el.bbox();
|
||||
this.nested.matrix(new SVG.Matrix(this.el).translate(bbox.x, bbox.y));
|
||||
|
||||
if (this.rectSelection.isSelected) {
|
||||
this.updateRectSelection();
|
||||
}
|
||||
|
||||
if (this.pointSelection.isSelected) {
|
||||
this.updatePointSelection();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
SelectHandler.prototype.observe = function () {
|
||||
var _this = this;
|
||||
|
||||
if (MutationObserver) {
|
||||
if (this.rectSelection.isSelected || this.pointSelection.isSelected) {
|
||||
this.observerInst = this.observerInst || new MutationObserver(function () {
|
||||
_this.handler();
|
||||
});
|
||||
this.observerInst.observe(this.el.node, {attributes: true});
|
||||
} else {
|
||||
try {
|
||||
this.observerInst.disconnect();
|
||||
delete this.observerInst;
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.el.off('DOMAttrModified.select');
|
||||
|
||||
if (this.rectSelection.isSelected || this.pointSelection.isSelected) {
|
||||
this.el.on('DOMAttrModified.select', function () {
|
||||
_this.handler();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
SelectHandler.prototype.cleanup = function () {
|
||||
|
||||
//var _this = this;
|
||||
|
||||
if (!this.rectSelection.isSelected && this.rectSelection.set) {
|
||||
// stop watching the element, remove the selection
|
||||
this.rectSelection.set.each(function () {
|
||||
this.remove();
|
||||
});
|
||||
|
||||
this.rectSelection.set.clear();
|
||||
delete this.rectSelection.set;
|
||||
}
|
||||
|
||||
if (!this.pointSelection.isSelected && this.pointSelection.set) {
|
||||
// Remove all points, clear the set, stop watching the element
|
||||
this.pointSelection.set.each(function () {
|
||||
this.remove();
|
||||
});
|
||||
|
||||
this.pointSelection.set.clear();
|
||||
delete this.pointSelection.set;
|
||||
}
|
||||
|
||||
if (!this.pointSelection.isSelected && !this.rectSelection.isSelected) {
|
||||
this.nested.remove();
|
||||
delete this.nested;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
SVG.extend(SVG.Element, {
|
||||
// Select element with mouse
|
||||
selectize: function (value, options) {
|
||||
|
||||
// Check the parameters and reassign if needed
|
||||
if (typeof value === 'object') {
|
||||
options = value;
|
||||
value = true;
|
||||
}
|
||||
|
||||
var selectHandler = this.remember('_selectHandler') || new SelectHandler(this);
|
||||
|
||||
selectHandler.init(value === undefined ? true : value, options || {});
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
SVG.Element.prototype.selectize.defaults = {
|
||||
points: ['lt', 'rt', 'rb', 'lb', 't', 'r', 'b', 'l'], // which points to draw, default all
|
||||
pointsExclude: [], // easier option if to exclude few than rewrite all
|
||||
classRect: 'svg_select_boundingRect', // Css-class added to the rect
|
||||
classPoints: 'svg_select_points', // Css-class added to the points
|
||||
pointSize: 7, // size of point
|
||||
rotationPoint: true, // If true, rotation point is drawn. Needed for rotation!
|
||||
deepSelect: false, // If true, moving of single points is possible (only line, polyline, polyon)
|
||||
pointType: 'circle' // Point type: circle or rect, default circle
|
||||
};
|
||||
}());
|
||||
@ -1,439 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* exported AnnotationParser */
|
||||
|
||||
/* global
|
||||
PolyShapeModel:false
|
||||
LabelsInfo:false
|
||||
*/
|
||||
|
||||
class AnnotationParser {
|
||||
constructor(job, labelsInfo) {
|
||||
this._parser = new DOMParser();
|
||||
this._startFrame = job.start;
|
||||
this._stopFrame = job.stop;
|
||||
this._im_meta = job.image_meta_data;
|
||||
this._labelsInfo = labelsInfo;
|
||||
}
|
||||
|
||||
_xmlParseError(parsedXML) {
|
||||
return parsedXML.getElementsByTagName('parsererror');
|
||||
}
|
||||
|
||||
_getBoxPosition(box, frame) {
|
||||
frame = Math.min(frame - this._startFrame, this._im_meta.length - 1);
|
||||
const imWidth = this._im_meta[frame].width;
|
||||
const imHeight = this._im_meta[frame].height;
|
||||
|
||||
const xtl = +box.getAttribute("xtl");
|
||||
const ytl = +box.getAttribute("ytl");
|
||||
const xbr = +box.getAttribute("xbr");
|
||||
const ybr = +box.getAttribute("ybr");
|
||||
|
||||
if (xtl < 0 || ytl < 0 || xbr < 0 || ybr < 0
|
||||
|| xtl > imWidth || ytl > imHeight || xbr > imWidth || ybr > imHeight) {
|
||||
const message = `Incorrect bb found in annotation file: xtl=${xtl} `
|
||||
+ `ytl=${ytl} xbr=${xbr} ybr=${ybr}. `
|
||||
+ `Box out of range: ${imWidth}x${imHeight}`;
|
||||
throw Error(message);
|
||||
}
|
||||
|
||||
const occluded = box.getAttribute('occluded');
|
||||
const zOrder = box.getAttribute('z_order') || '0';
|
||||
return [[xtl, ytl, xbr, ybr], +occluded, +zOrder];
|
||||
}
|
||||
|
||||
_getPolyPosition(shape, frame) {
|
||||
frame = Math.min(frame - this._startFrame, this._im_meta.length - 1);
|
||||
const imWidth = this._im_meta[frame].width;
|
||||
const imHeight = this._im_meta[frame].height;
|
||||
let points = shape.getAttribute('points').split(';').join(' ');
|
||||
points = PolyShapeModel.convertStringToNumberArray(points);
|
||||
|
||||
for (const point of points) {
|
||||
if (point.x < 0 || point.y < 0 || point.x > imWidth || point.y > imHeight) {
|
||||
const message = `Incorrect point found in annotation file x=${point.x} `
|
||||
+ `y=${point.y}. Point out of range ${imWidth}x${imHeight}`;
|
||||
throw Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
points = points.reduce((acc, el) => {
|
||||
acc.push(el.x, el.y);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const occluded = shape.getAttribute('occluded');
|
||||
const zOrder = shape.getAttribute('z_order') || '0';
|
||||
return [points, +occluded, +zOrder];
|
||||
}
|
||||
|
||||
_getAttribute(labelId, attrTag) {
|
||||
const name = attrTag.getAttribute('name');
|
||||
const attrId = this._labelsInfo.attrIdOf(labelId, name);
|
||||
if (attrId === null) {
|
||||
throw Error(`An unknown attribute found in the annotation file: ${name}`);
|
||||
}
|
||||
const attrInfo = this._labelsInfo.attrInfo(attrId);
|
||||
const value = LabelsInfo.normalize(attrInfo.type, attrTag.textContent);
|
||||
|
||||
if (['select', 'radio'].includes(attrInfo.type) && !attrInfo.values.includes(value)) {
|
||||
throw Error(`Incorrect attribute value found for "${name}" + attribute: "${value}"`);
|
||||
} else if (attrInfo.type === 'number') {
|
||||
if (Number.isNaN(+value)) {
|
||||
throw Error(`Incorrect attribute value found for "${name}" attribute: "${value}". Value must be a number.`);
|
||||
} else {
|
||||
const min = +attrInfo.values[0];
|
||||
const max = +attrInfo.values[1];
|
||||
if (+value < min || +value > max) {
|
||||
throw Error(`Number attribute value out of range for "${name}" attribute: "${value}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [attrId, value];
|
||||
}
|
||||
|
||||
_getAttributeList(shape, labelId) {
|
||||
const attributeDict = {};
|
||||
const attributes = shape.getElementsByTagName('attribute');
|
||||
for (const attribute of attributes) {
|
||||
const [id, value] = this._getAttribute(labelId, attribute);
|
||||
attributeDict[id] = value;
|
||||
}
|
||||
|
||||
const attributeList = [];
|
||||
for (const attrId in attributeDict) {
|
||||
if (Object.prototype.hasOwnProperty.call(attributeDict, attrId)) {
|
||||
attributeList.push({
|
||||
spec_id: attrId,
|
||||
value: attributeDict[attrId],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return attributeList;
|
||||
}
|
||||
|
||||
_getShapeFromPath(shapeType, tracks) {
|
||||
const result = [];
|
||||
for (const track of tracks) {
|
||||
const label = track.getAttribute('label');
|
||||
const group = track.getAttribute('group_id') || '0';
|
||||
const labelId = this._labelsInfo.labelIdOf(label);
|
||||
if (labelId === null) {
|
||||
throw Error(`An unknown label found in the annotation file: ${label}`);
|
||||
}
|
||||
|
||||
const shapes = Array.from(track.getElementsByTagName(shapeType));
|
||||
shapes.sort((a, b) => +a.getAttribute('frame') - +b.getAttribute('frame'));
|
||||
|
||||
while (shapes.length && +shapes[0].getAttribute('outside')) {
|
||||
shapes.shift();
|
||||
}
|
||||
|
||||
if (shapes.length === 2) {
|
||||
if (shapes[1].getAttribute('frame') - shapes[0].getAttribute('frame') === 1
|
||||
&& !+shapes[0].getAttribute('outside') && +shapes[1].getAttribute('outside')) {
|
||||
shapes[0].setAttribute('label', label);
|
||||
shapes[0].setAttribute('group_id', group);
|
||||
result.push(shapes[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
_parseAnnotationData(xml) {
|
||||
const data = {
|
||||
boxes: [],
|
||||
polygons: [],
|
||||
polylines: [],
|
||||
points: [],
|
||||
cuboids: [],
|
||||
};
|
||||
|
||||
const tracks = xml.getElementsByTagName('track');
|
||||
const parsed = {
|
||||
box: this._getShapeFromPath('box', tracks),
|
||||
polygon: this._getShapeFromPath('polygon', tracks),
|
||||
polyline: this._getShapeFromPath('polyline', tracks),
|
||||
points: this._getShapeFromPath('points', tracks),
|
||||
cuboid: this._getShapeFromPath('cuboid', tracks),
|
||||
};
|
||||
const shapeTarget = {
|
||||
box: 'boxes',
|
||||
polygon: 'polygons',
|
||||
polyline: 'polylines',
|
||||
points: 'points',
|
||||
cuboid: 'cuboids',
|
||||
};
|
||||
|
||||
const images = xml.getElementsByTagName('image');
|
||||
for (const image of images) {
|
||||
const frame = image.getAttribute('id');
|
||||
|
||||
for (const box of image.getElementsByTagName('box')) {
|
||||
box.setAttribute('frame', frame);
|
||||
parsed.box.push(box);
|
||||
}
|
||||
|
||||
for (const polygon of image.getElementsByTagName('polygon')) {
|
||||
polygon.setAttribute('frame', frame);
|
||||
parsed.polygon.push(polygon);
|
||||
}
|
||||
|
||||
for (const polyline of image.getElementsByTagName('polyline')) {
|
||||
polyline.setAttribute('frame', frame);
|
||||
parsed.polyline.push(polyline);
|
||||
}
|
||||
|
||||
for (const points of image.getElementsByTagName('points')) {
|
||||
points.setAttribute('frame', frame);
|
||||
parsed.points.push(points);
|
||||
}
|
||||
for (const cuboid of image.getElementsByTagName('cuboid')) {
|
||||
cuboid.setAttribute('frame', frame);
|
||||
parsed.cuboid.push(cuboid);
|
||||
}
|
||||
}
|
||||
|
||||
for (const shapeType in parsed) {
|
||||
if (Object.prototype.hasOwnProperty.call(parsed, shapeType)) {
|
||||
for (const shape of parsed[shapeType]) {
|
||||
const frame = +shape.getAttribute('frame');
|
||||
if (frame < this._startFrame || frame > this._stopFrame) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const labelId = this._labelsInfo.labelIdOf(shape.getAttribute('label'));
|
||||
const group = shape.getAttribute('group_id') || '0';
|
||||
if (labelId === null) {
|
||||
throw Error(`An unknown label found in the annotation file: "${shape.getAttribute('label')}"`);
|
||||
}
|
||||
|
||||
const attributeList = this._getAttributeList(shape, labelId);
|
||||
|
||||
if (shapeType === 'box') {
|
||||
const [points, occluded, zOrder] = this._getBoxPosition(shape, frame);
|
||||
data[shapeTarget[shapeType]].push({
|
||||
label_id: labelId,
|
||||
group: +group,
|
||||
attributes: attributeList,
|
||||
type: 'rectangle',
|
||||
z_order: zOrder,
|
||||
frame,
|
||||
occluded,
|
||||
points,
|
||||
});
|
||||
} else if (shapeType === 'cuboid') {
|
||||
const [points, occluded, zOrder] = this._getPolyPosition(shape, frame);
|
||||
data[shapeTarget[shapeType]].push({
|
||||
label_id: labelId,
|
||||
group: +group,
|
||||
attributes: attributeList,
|
||||
type: 'cuboid',
|
||||
z_order: zOrder,
|
||||
frame,
|
||||
occluded,
|
||||
points,
|
||||
});
|
||||
} else {
|
||||
const [points, occluded, zOrder] = this._getPolyPosition(shape, frame);
|
||||
data[shapeTarget[shapeType]].push({
|
||||
label_id: labelId,
|
||||
group: +group,
|
||||
attributes: attributeList,
|
||||
type: shapeType,
|
||||
z_order: zOrder,
|
||||
frame,
|
||||
points,
|
||||
occluded,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
_parseInterpolationData(xml) {
|
||||
const data = {
|
||||
box_paths: [],
|
||||
polygon_paths: [],
|
||||
polyline_paths: [],
|
||||
points_paths: [],
|
||||
};
|
||||
|
||||
const tracks = xml.getElementsByTagName('track');
|
||||
for (const track of tracks) {
|
||||
const labelId = this._labelsInfo.labelIdOf(track.getAttribute('label'));
|
||||
const group = track.getAttribute('group_id') || '0';
|
||||
if (labelId === null) {
|
||||
throw Error(`An unknown label found in the annotation file: "${track.getAttribute('label')}"`);
|
||||
}
|
||||
|
||||
const parsed = {
|
||||
box: Array.from(track.getElementsByTagName('box')),
|
||||
polygon: Array.from(track.getElementsByTagName('polygon')),
|
||||
polyline: Array.from(track.getElementsByTagName('polyline')),
|
||||
points: Array.from(track.getElementsByTagName('points')),
|
||||
};
|
||||
|
||||
for (const shapeType in parsed) {
|
||||
if (Object.prototype.hasOwnProperty.call(parsed, shapeType)) {
|
||||
const shapes = parsed[shapeType];
|
||||
shapes.sort((a, b) => +a.getAttribute('frame') - +b.getAttribute('frame'));
|
||||
|
||||
while (shapes.length && +shapes[0].getAttribute('outside')) {
|
||||
shapes.shift();
|
||||
}
|
||||
|
||||
if (shapes.length === 2) {
|
||||
if (shapes[1].getAttribute('frame') - shapes[0].getAttribute('frame') === 1
|
||||
&& !+shapes[0].getAttribute('outside') && +shapes[1].getAttribute('outside')) {
|
||||
// pseudo interpolation track (actually is annotation)
|
||||
parsed[shapeType] = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let type = null;
|
||||
let target = null;
|
||||
if (parsed.box.length) {
|
||||
type = 'box';
|
||||
target = 'box_paths';
|
||||
} else if (parsed.polygon.length) {
|
||||
type = 'polygon';
|
||||
target = 'polygon_paths';
|
||||
} else if (parsed.polyline.length) {
|
||||
type = 'polyline';
|
||||
target = 'polyline_paths';
|
||||
} else if (parsed.points.length) {
|
||||
type = 'points';
|
||||
target = 'points_paths';
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
const path = {
|
||||
label_id: labelId,
|
||||
group: +group,
|
||||
frame: +parsed[type][0].getAttribute('frame'),
|
||||
attributes: [],
|
||||
shapes: [],
|
||||
};
|
||||
|
||||
if (path.frame > this._stopFrame) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const shape of parsed[type]) {
|
||||
const keyFrame = +shape.getAttribute('keyframe');
|
||||
const outside = +shape.getAttribute('outside');
|
||||
const frame = +shape.getAttribute('frame');
|
||||
|
||||
/*
|
||||
All keyframes are significant.
|
||||
All shapes on first segment frame also significant.
|
||||
Ignore all frames less then start.
|
||||
Ignore all frames more then stop.
|
||||
*/
|
||||
const significant = (keyFrame || frame === this._startFrame)
|
||||
&& frame >= this._startFrame && frame <= this._stopFrame;
|
||||
|
||||
if (significant) {
|
||||
const attributeList = this._getAttributeList(shape, labelId);
|
||||
const shapeAttributes = [];
|
||||
const pathAttributes = [];
|
||||
|
||||
for (const attr of attributeList) {
|
||||
const attrInfo = this._labelsInfo.attrInfo(attr.spec_id);
|
||||
if (attrInfo.mutable) {
|
||||
shapeAttributes.push({
|
||||
spec_id: attr.spec_id,
|
||||
value: attr.value,
|
||||
});
|
||||
} else {
|
||||
pathAttributes.push({
|
||||
spec_id: attr.spec_id,
|
||||
value: attr.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
path.attributes = pathAttributes;
|
||||
|
||||
if (type === 'box') {
|
||||
const [points, occluded, zOrder] = this._getBoxPosition(shape,
|
||||
Math.clamp(frame, this._startFrame, this._stopFrame));
|
||||
path.shapes.push({
|
||||
attributes: shapeAttributes,
|
||||
type: 'rectangle',
|
||||
frame,
|
||||
occluded,
|
||||
outside,
|
||||
points,
|
||||
zOrder,
|
||||
});
|
||||
} else {
|
||||
const [points, occluded, zOrder] = this._getPolyPosition(shape,
|
||||
Math.clamp(frame, this._startFrame, this._stopFrame));
|
||||
path.shapes.push({
|
||||
attributes: shapeAttributes,
|
||||
type,
|
||||
frame,
|
||||
occluded,
|
||||
outside,
|
||||
points,
|
||||
zOrder,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (path.shapes.length) {
|
||||
data[target].push(path);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
parse(text) {
|
||||
const xml = this._parser.parseFromString(text, 'text/xml');
|
||||
const parseerror = this._xmlParseError(xml);
|
||||
if (parseerror.length) {
|
||||
throw Error(`Annotation page parsing error. ${parseerror[0].innerText}`);
|
||||
}
|
||||
|
||||
const interpolationData = this._parseInterpolationData(xml);
|
||||
const annotationData = this._parseAnnotationData(xml);
|
||||
|
||||
const data = {
|
||||
shapes: [],
|
||||
tracks: [],
|
||||
};
|
||||
|
||||
|
||||
for (const type in interpolationData) {
|
||||
if (Object.prototype.hasOwnProperty.call(interpolationData, type)) {
|
||||
Array.prototype.push.apply(data.tracks, interpolationData[type]);
|
||||
}
|
||||
}
|
||||
|
||||
for (const type in annotationData) {
|
||||
if (Object.prototype.hasOwnProperty.call(annotationData, type)) {
|
||||
Array.prototype.push.apply(data.shapes, annotationData[type]);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@ -1,452 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* exported buildAnnotationSaver */
|
||||
|
||||
/* global
|
||||
showOverlay:false
|
||||
showMessage:false
|
||||
Listener:false
|
||||
Logger:false
|
||||
Mousetrap:false
|
||||
*/
|
||||
|
||||
|
||||
class AnnotationSaverModel extends Listener {
|
||||
constructor(initialData, shapeCollection) {
|
||||
super('onAnnotationSaverUpdate', () => this._state);
|
||||
|
||||
this._state = {
|
||||
status: null,
|
||||
message: null,
|
||||
};
|
||||
|
||||
this._version = initialData.version;
|
||||
this._shapeCollection = shapeCollection;
|
||||
this._initialObjects = {};
|
||||
|
||||
this._resetState();
|
||||
this.update();
|
||||
|
||||
// We need use data from export instead of initialData
|
||||
// Otherwise we have differ keys order and JSON comparison code incorrect
|
||||
const data = this._shapeCollection.export()[0];
|
||||
for (const shape of data.shapes) {
|
||||
this._initialObjects.shapes[shape.id] = shape;
|
||||
}
|
||||
|
||||
for (const track of data.tracks) {
|
||||
this._initialObjects.tracks[track.id] = track;
|
||||
}
|
||||
}
|
||||
|
||||
_resetState() {
|
||||
this._initialObjects = {
|
||||
shapes: {},
|
||||
tracks: {},
|
||||
};
|
||||
}
|
||||
|
||||
update() {
|
||||
this._hash = this._getHash();
|
||||
}
|
||||
|
||||
async _request(data, action) {
|
||||
return new Promise((resolve, reject) => {
|
||||
$.ajax({
|
||||
url: `/api/v1/jobs/${window.cvat.job.id}/annotations?action=${action}`,
|
||||
type: 'PATCH',
|
||||
data: JSON.stringify(data),
|
||||
contentType: 'application/json',
|
||||
}).done((savedData) => {
|
||||
resolve(savedData);
|
||||
}).fail((errorData) => {
|
||||
const message = `Could not make ${action} annotations. Code: ${errorData.status}. `
|
||||
+ `Message: ${errorData.responseText || errorData.statusText}`;
|
||||
reject(new Error(message));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async _put(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
$.ajax({
|
||||
url: `/api/v1/jobs/${window.cvat.job.id}/annotations`,
|
||||
type: 'PUT',
|
||||
data: JSON.stringify(data),
|
||||
contentType: 'application/json',
|
||||
}).done((savedData) => {
|
||||
resolve(savedData);
|
||||
}).fail((errorData) => {
|
||||
const message = `Could not put annotations. Code: ${errorData.status}. `
|
||||
+ `Message: ${errorData.responseText || errorData.statusText}`;
|
||||
reject(new Error(message));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async _create(created) {
|
||||
return this._request(created, 'create');
|
||||
}
|
||||
|
||||
async _update(updated) {
|
||||
return this._request(updated, 'update');
|
||||
}
|
||||
|
||||
async _delete(deleted) {
|
||||
return this._request(deleted, 'delete');
|
||||
}
|
||||
|
||||
async _logs() {
|
||||
Logger.addEvent(Logger.EventType.saveJob);
|
||||
const totalStat = this._shapeCollection.collectStatistic()[1];
|
||||
Logger.addEvent(Logger.EventType.sendTaskInfo, {
|
||||
'track count': totalStat.boxes.annotation + totalStat.boxes.interpolation
|
||||
+ totalStat.polygons.annotation + totalStat.polygons.interpolation
|
||||
+ totalStat.polylines.annotation + totalStat.polylines.interpolation
|
||||
+ totalStat.points.annotation + totalStat.points.interpolation,
|
||||
'frame count': window.cvat.player.frames.stop - window.cvat.player.frames.start + 1,
|
||||
'object count': totalStat.total,
|
||||
'box count': totalStat.boxes.annotation + totalStat.boxes.interpolation,
|
||||
'polygon count': totalStat.polygons.annotation + totalStat.polygons.interpolation,
|
||||
'polyline count': totalStat.polylines.annotation + totalStat.polylines.interpolation,
|
||||
'points count': totalStat.points.annotation + totalStat.points.interpolation,
|
||||
});
|
||||
|
||||
const annotationLogs = Logger.getLogs();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
$.ajax({
|
||||
url: '/api/v1/server/logs',
|
||||
type: 'POST',
|
||||
data: JSON.stringify(annotationLogs.export()),
|
||||
contentType: 'application/json',
|
||||
}).done(() => {
|
||||
resolve();
|
||||
}).fail((errorData) => {
|
||||
annotationLogs.save();
|
||||
const message = `Could not send logs. Code: ${errorData.status}. `
|
||||
+ `Message: ${errorData.responseText || errorData.statusText}`;
|
||||
reject(new Error(message));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_split(exported) {
|
||||
const created = {
|
||||
version: this._version,
|
||||
shapes: [],
|
||||
tracks: [],
|
||||
tags: [],
|
||||
};
|
||||
|
||||
const updated = {
|
||||
version: this._version + 1,
|
||||
shapes: [],
|
||||
tracks: [],
|
||||
tags: [],
|
||||
};
|
||||
|
||||
const deleted = {
|
||||
version: this._version + 2,
|
||||
shapes: [],
|
||||
tracks: [],
|
||||
tags: [],
|
||||
};
|
||||
|
||||
const keys = ['id', 'label_id', 'group', 'frame',
|
||||
'occluded', 'z_order', 'points', 'type', 'shapes',
|
||||
'attributes', 'value', 'spec_id', 'outside'];
|
||||
|
||||
// Compare initial state objects and export state objects
|
||||
// in order to get updated and created objects
|
||||
for (const type of Object.keys(this._initialObjects)) {
|
||||
for (const obj of exported[type]) {
|
||||
if (obj.id in this._initialObjects[type]) {
|
||||
const exportedHash = JSON.stringify(obj, keys);
|
||||
const initialHash = JSON.stringify(this._initialObjects[type][obj.id], keys);
|
||||
if (exportedHash !== initialHash) {
|
||||
updated[type].push(obj);
|
||||
}
|
||||
} else if (typeof obj.id === 'undefined') {
|
||||
created[type].push(obj);
|
||||
} else {
|
||||
throw Error(`Bad object ID found: ${obj.id}. `
|
||||
+ 'It is not contained in initial state and have server ID');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const indexes = {
|
||||
shapes: exported.shapes.map((object) => +object.id),
|
||||
tracks: exported.tracks.map((object) => +object.id),
|
||||
};
|
||||
|
||||
// Compare initial state indexes and export state indexes
|
||||
// in order to get removed objects
|
||||
for (const type of Object.keys(this._initialObjects)) {
|
||||
for (const shapeID in this._initialObjects[type]) {
|
||||
if (!indexes[type].includes(+shapeID)) {
|
||||
const object = this._initialObjects[type][shapeID];
|
||||
deleted[type].push(object);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [created, updated, deleted];
|
||||
}
|
||||
|
||||
_getHash() {
|
||||
const exported = this._shapeCollection.export()[0];
|
||||
return JSON.stringify(exported);
|
||||
}
|
||||
|
||||
_updateCreatedObjects(objectsToSave, savedObjects, mapping) {
|
||||
// Method setups IDs of created objects after saving on a server
|
||||
const allSavedObjects = savedObjects.shapes.concat(savedObjects.tracks);
|
||||
const allObjectsToSave = objectsToSave.shapes.concat(objectsToSave.tracks);
|
||||
if (allSavedObjects.length !== allObjectsToSave.length) {
|
||||
throw Error('Number of saved objects and objects to save is not match');
|
||||
}
|
||||
|
||||
for (let idx = 0; idx < allSavedObjects.length; idx += 1) {
|
||||
const objectModel = mapping.filter(el => el[0] === allObjectsToSave[idx])[0][1];
|
||||
const { id } = allSavedObjects[idx];
|
||||
objectModel.serverID = id;
|
||||
allObjectsToSave[idx].id = id;
|
||||
}
|
||||
|
||||
this._shapeCollection.update();
|
||||
}
|
||||
|
||||
notify(status, message = null) {
|
||||
this._state.status = status;
|
||||
this._state.message = message;
|
||||
Listener.prototype.notify.call(this);
|
||||
}
|
||||
|
||||
hasUnsavedChanges() {
|
||||
return this._getHash() !== this._hash;
|
||||
}
|
||||
|
||||
async save() {
|
||||
this.notify('saveStart');
|
||||
try {
|
||||
const [exported, mapping] = this._shapeCollection.export();
|
||||
const { flush } = this._shapeCollection;
|
||||
if (flush) {
|
||||
const data = Object.assign({}, exported, {
|
||||
version: this._version,
|
||||
tags: [],
|
||||
});
|
||||
|
||||
this._version += 1;
|
||||
|
||||
this.notify('saveCreated');
|
||||
const savedObjects = await this._put(data);
|
||||
this._updateCreatedObjects(exported, savedObjects, mapping);
|
||||
this._shapeCollection.flush = false;
|
||||
this._version = savedObjects.version;
|
||||
this._resetState();
|
||||
|
||||
for (const type of Object.keys(this._initialObjects)) {
|
||||
for (const object of savedObjects[type]) {
|
||||
if (object.shapes) {
|
||||
for (const shape of object.shapes) {
|
||||
delete shape.id;
|
||||
}
|
||||
}
|
||||
this._initialObjects[type][object.id] = object;
|
||||
}
|
||||
}
|
||||
|
||||
this._version = savedObjects.version;
|
||||
} else {
|
||||
const [created, updated, deleted] = this._split(exported);
|
||||
this.notify('saveCreated');
|
||||
const savedCreated = await this._create(created);
|
||||
this._updateCreatedObjects(created, savedCreated, mapping);
|
||||
this._version = savedCreated.version;
|
||||
|
||||
for (const type of Object.keys(this._initialObjects)) {
|
||||
for (const object of savedCreated[type]) {
|
||||
if (object.shapes) {
|
||||
for (const shape of object.shapes) {
|
||||
delete shape.id;
|
||||
}
|
||||
}
|
||||
this._initialObjects[type][object.id] = object;
|
||||
}
|
||||
}
|
||||
|
||||
this.notify('saveUpdated');
|
||||
const savedUpdated = await this._update(updated);
|
||||
this._version = savedUpdated.version;
|
||||
for (const type of Object.keys(this._initialObjects)) {
|
||||
for (const object of savedUpdated[type]) {
|
||||
if (object.shapes) {
|
||||
for (const shape of object.shapes) {
|
||||
delete shape.id;
|
||||
}
|
||||
}
|
||||
this._initialObjects[type][object.id] = object;
|
||||
}
|
||||
}
|
||||
|
||||
this.notify('saveDeleted');
|
||||
const savedDeleted = await this._delete(deleted);
|
||||
this._version = savedDeleted.version;
|
||||
for (const type of Object.keys(this._initialObjects)) {
|
||||
for (const object of savedDeleted[type]) {
|
||||
delete this._initialObjects[type][object.id];
|
||||
}
|
||||
}
|
||||
|
||||
this._version = savedDeleted.version;
|
||||
}
|
||||
|
||||
await this._logs();
|
||||
} catch (error) {
|
||||
this.notify('saveUnlocked');
|
||||
this.notify('saveError', error.message);
|
||||
this._state = {
|
||||
status: null,
|
||||
message: null,
|
||||
};
|
||||
throw Error(error);
|
||||
}
|
||||
|
||||
this._hash = this._getHash();
|
||||
this.notify('saveDone');
|
||||
|
||||
setTimeout(() => {
|
||||
this.notify('saveUnlocked');
|
||||
this._state = {
|
||||
status: null,
|
||||
message: null,
|
||||
};
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
get state() {
|
||||
return JSON.parse(JSON.stringify(this._state));
|
||||
}
|
||||
}
|
||||
|
||||
class AnnotationSaverController {
|
||||
constructor(model) {
|
||||
this._model = model;
|
||||
this._autoSaveInterval = null;
|
||||
|
||||
const { shortkeys } = window.cvat.config;
|
||||
Mousetrap.bind(shortkeys.save_work.value, Logger.shortkeyLogDecorator(
|
||||
() => {
|
||||
this.save();
|
||||
return false;
|
||||
},
|
||||
), 'keydown');
|
||||
}
|
||||
|
||||
autoSave(enabled, time) {
|
||||
if (this._autoSaveInterval) {
|
||||
clearInterval(this._autoSaveInterval);
|
||||
this._autoSaveInterval = null;
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
this._autoSaveInterval = setInterval(() => {
|
||||
this.save();
|
||||
}, time * 1000 * 60);
|
||||
}
|
||||
}
|
||||
|
||||
hasUnsavedChanges() {
|
||||
return this._model.hasUnsavedChanges();
|
||||
}
|
||||
|
||||
save() {
|
||||
if (this._model.state.status === null) {
|
||||
this._model.save().catch((error) => {
|
||||
setTimeout(() => {
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AnnotationSaverView {
|
||||
constructor(model, controller) {
|
||||
model.subscribe(this);
|
||||
|
||||
this._controller = controller;
|
||||
this._overlay = null;
|
||||
|
||||
const { shortkeys } = window.cvat.config;
|
||||
const saveHelp = `${shortkeys.save_work.view_value} - ${shortkeys.save_work.description}`;
|
||||
|
||||
this._saveButton = $('#saveButton').on('click', () => {
|
||||
this._controller.save();
|
||||
}).attr('title', saveHelp);
|
||||
|
||||
this._autoSaveBox = $('#autoSaveBox').on('change', (e) => {
|
||||
const enabled = e.target.checked;
|
||||
const time = +this._autoSaveTime.prop('value');
|
||||
this._controller.autoSave(enabled, time);
|
||||
});
|
||||
|
||||
this._autoSaveTime = $('#autoSaveTime').on('change', (e) => {
|
||||
e.target.value = Math.clamp(+e.target.value, +e.target.min, +e.target.max);
|
||||
this._autoSaveBox.trigger('change');
|
||||
});
|
||||
|
||||
window.onbeforeunload = (e) => {
|
||||
if (this._controller.hasUnsavedChanges()) {
|
||||
const message = 'You have unsaved changes. Leave this page?';
|
||||
e.returnValue = message;
|
||||
return message;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
onAnnotationSaverUpdate(state) {
|
||||
if (state.status === 'saveStart') {
|
||||
this._overlay = showOverlay('Annotations are being saved..');
|
||||
this._saveButton.prop('disabled', true).text('Saving..');
|
||||
} else if (state.status === 'saveDone') {
|
||||
this._saveButton.text('Successful save');
|
||||
this._overlay.remove();
|
||||
} else if (state.status === 'saveError') {
|
||||
this._saveButton.prop('disabled', false).text('Save Work');
|
||||
const message = `Couldn't to save the job. Errors occured: ${state.message}. `
|
||||
+ 'Please report the problem to support team immediately.';
|
||||
showMessage(message);
|
||||
this._overlay.remove();
|
||||
} else if (state.status === 'saveCreated') {
|
||||
this._overlay.setMessage(`${this._overlay.getMessage()} <br /> - Created objects are being saved..`);
|
||||
} else if (state.status === 'saveUpdated') {
|
||||
this._overlay.setMessage(`${this._overlay.getMessage()} <br /> - Updated objects are being saved..`);
|
||||
} else if (state.status === 'saveDeleted') {
|
||||
this._overlay.setMessage(`${this._overlay.getMessage()} <br /> - Deleted objects are being saved..`);
|
||||
} else if (state.status === 'saveUnlocked') {
|
||||
this._saveButton.prop('disabled', false).text('Save Work');
|
||||
} else {
|
||||
const message = `Unknown state has been reached during annotation saving: ${state.status} `
|
||||
+ 'Please report the problem to support team immediately.';
|
||||
showMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function buildAnnotationSaver(initialData, shapeCollection) {
|
||||
const model = new AnnotationSaverModel(initialData, shapeCollection);
|
||||
const controller = new AnnotationSaverController(model);
|
||||
new AnnotationSaverView(model, controller);
|
||||
return model;
|
||||
}
|
||||
@ -1,761 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018-2019 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* exported callAnnotationUI blurAllElements drawBoxSize copyToClipboard */
|
||||
|
||||
/* global
|
||||
AAMController:false
|
||||
AAMModel:false
|
||||
AAMView:false
|
||||
AnnotationParser:false
|
||||
Config:false
|
||||
userConfirm:false
|
||||
CoordinateTranslator:false
|
||||
dumpAnnotationRequest:false
|
||||
HistoryController:false
|
||||
HistoryModel:false
|
||||
HistoryView:false
|
||||
Logger:false
|
||||
Mousetrap:false
|
||||
PlayerController:false
|
||||
PlayerModel:false
|
||||
PlayerView:false
|
||||
PolyshapeEditorController:false
|
||||
PolyshapeEditorModel:false
|
||||
PolyshapeEditorView:false
|
||||
PolyShapeView:false
|
||||
ShapeBufferController:false
|
||||
ShapeBufferModel:false
|
||||
ShapeBufferView:false
|
||||
ShapeCollectionController:false
|
||||
ShapeCollectionModel:false
|
||||
ShapeCollectionView:false
|
||||
ShapeCreatorController:false
|
||||
ShapeCreatorModel:false
|
||||
ShapeCreatorView:false
|
||||
ShapeGrouperController:false
|
||||
ShapeGrouperModel:false
|
||||
ShapeGrouperView:false
|
||||
ShapeMergerController:false
|
||||
ShapeMergerModel:false
|
||||
ShapeMergerView:false
|
||||
showMessage:false
|
||||
buildAnnotationSaver:false
|
||||
LabelsInfo:false
|
||||
uploadJobAnnotationRequest:false
|
||||
isDefaultFormat:false
|
||||
*/
|
||||
|
||||
async function initLogger(jobID) {
|
||||
if (!Logger.initializeLogger(jobID)) {
|
||||
const message = 'Logger has been already initialized';
|
||||
console.error(message);
|
||||
showMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.setTimeThreshold(Logger.EventType.zoomImage);
|
||||
}
|
||||
|
||||
|
||||
function blurAllElements() {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
function uploadAnnotation(jobId, shapeCollectionModel, historyModel, annotationSaverModel,
|
||||
uploadAnnotationButton, format) {
|
||||
$('#annotationFileSelector').attr('accept',
|
||||
format.ext.split(',').map(x => '.' + x.trimStart()).join(', '));
|
||||
$('#annotationFileSelector').one('change', async (changedFileEvent) => {
|
||||
const file = changedFileEvent.target.files['0'];
|
||||
changedFileEvent.target.value = '';
|
||||
if (!file) return;
|
||||
uploadAnnotationButton.prop('disabled', true);
|
||||
const annotationData = new FormData();
|
||||
annotationData.append('annotation_file', file);
|
||||
try {
|
||||
await uploadJobAnnotationRequest(jobId, annotationData, format.name);
|
||||
historyModel.empty();
|
||||
shapeCollectionModel.empty();
|
||||
const data = await $.get(`/api/v1/jobs/${jobId}/annotations`);
|
||||
shapeCollectionModel.import(data);
|
||||
shapeCollectionModel.update();
|
||||
annotationSaverModel.update();
|
||||
} catch (error) {
|
||||
showMessage(error.message);
|
||||
} finally {
|
||||
uploadAnnotationButton.prop('disabled', false);
|
||||
}
|
||||
}).click();
|
||||
}
|
||||
|
||||
|
||||
function setupFrameFilters() {
|
||||
const brightnessRange = $('#playerBrightnessRange');
|
||||
const contrastRange = $('#playerContrastRange');
|
||||
const saturationRange = $('#playerSaturationRange');
|
||||
const canvasBackground = $('#canvasBackground');
|
||||
const reset = $('#resetPlayerFilterButton');
|
||||
let brightness = 100;
|
||||
let contrast = 100;
|
||||
let saturation = 100;
|
||||
|
||||
const { shortkeys } = window.cvat.config;
|
||||
|
||||
function updateFilterParameters() {
|
||||
canvasBackground.css('filter', `contrast(${contrast}%) brightness(${brightness}%) saturate(${saturation}%)`);
|
||||
}
|
||||
|
||||
brightnessRange.attr('title', `
|
||||
${shortkeys.change_player_brightness.view_value} - ${shortkeys.change_player_brightness.description}`);
|
||||
contrastRange.attr('title', `
|
||||
${shortkeys.change_player_contrast.view_value} - ${shortkeys.change_player_contrast.description}`);
|
||||
saturationRange.attr('title', `
|
||||
${shortkeys.change_player_saturation.view_value} - ${shortkeys.change_player_saturation.description}`);
|
||||
|
||||
const changeBrightnessHandler = Logger.shortkeyLogDecorator((e) => {
|
||||
if (e.shiftKey) {
|
||||
brightnessRange.prop('value', brightness + 10).trigger('input');
|
||||
} else {
|
||||
brightnessRange.prop('value', brightness - 10).trigger('input');
|
||||
}
|
||||
});
|
||||
|
||||
const changeContrastHandler = Logger.shortkeyLogDecorator((e) => {
|
||||
if (e.shiftKey) {
|
||||
contrastRange.prop('value', contrast + 10).trigger('input');
|
||||
} else {
|
||||
contrastRange.prop('value', contrast - 10).trigger('input');
|
||||
}
|
||||
});
|
||||
|
||||
const changeSaturationHandler = Logger.shortkeyLogDecorator((e) => {
|
||||
if (e.shiftKey) {
|
||||
saturationRange.prop('value', saturation + 10).trigger('input');
|
||||
} else {
|
||||
saturationRange.prop('value', saturation - 10).trigger('input');
|
||||
}
|
||||
});
|
||||
|
||||
Mousetrap.bind(shortkeys.change_player_brightness.value, changeBrightnessHandler, 'keydown');
|
||||
Mousetrap.bind(shortkeys.change_player_contrast.value, changeContrastHandler, 'keydown');
|
||||
Mousetrap.bind(shortkeys.change_player_saturation.value, changeSaturationHandler, 'keydown');
|
||||
|
||||
reset.on('click', () => {
|
||||
brightness = 100;
|
||||
contrast = 100;
|
||||
saturation = 100;
|
||||
brightnessRange.prop('value', brightness);
|
||||
contrastRange.prop('value', contrast);
|
||||
saturationRange.prop('value', saturation);
|
||||
updateFilterParameters();
|
||||
});
|
||||
|
||||
brightnessRange.on('input', (e) => {
|
||||
const value = Math.clamp(+e.target.value, +e.target.min, +e.target.max);
|
||||
e.target.value = value;
|
||||
brightness = value;
|
||||
updateFilterParameters();
|
||||
});
|
||||
|
||||
contrastRange.on('input', (e) => {
|
||||
const value = Math.clamp(+e.target.value, +e.target.min, +e.target.max);
|
||||
e.target.value = value;
|
||||
contrast = value;
|
||||
updateFilterParameters();
|
||||
});
|
||||
|
||||
saturationRange.on('input', (e) => {
|
||||
const value = Math.clamp(+e.target.value, +e.target.min, +e.target.max);
|
||||
e.target.value = value;
|
||||
saturation = value;
|
||||
updateFilterParameters();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function setupShortkeys(shortkeys, models) {
|
||||
const annotationMenu = $('#annotationMenu');
|
||||
const settingsWindow = $('#settingsWindow');
|
||||
const helpWindow = $('#helpWindow');
|
||||
|
||||
Mousetrap.prototype.stopCallback = () => false;
|
||||
|
||||
const openHelpHandler = Logger.shortkeyLogDecorator(() => {
|
||||
const helpInvisible = helpWindow.hasClass('hidden');
|
||||
if (helpInvisible) {
|
||||
annotationMenu.addClass('hidden');
|
||||
settingsWindow.addClass('hidden');
|
||||
helpWindow.removeClass('hidden');
|
||||
} else {
|
||||
helpWindow.addClass('hidden');
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const openSettingsHandler = Logger.shortkeyLogDecorator(() => {
|
||||
const settingsInvisible = settingsWindow.hasClass('hidden');
|
||||
if (settingsInvisible) {
|
||||
annotationMenu.addClass('hidden');
|
||||
helpWindow.addClass('hidden');
|
||||
settingsWindow.removeClass('hidden');
|
||||
} else {
|
||||
$('#settingsWindow').addClass('hidden');
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const cancelModeHandler = Logger.shortkeyLogDecorator(() => {
|
||||
switch (window.cvat.mode) {
|
||||
case 'aam':
|
||||
models.aam.switchAAMMode();
|
||||
break;
|
||||
case 'creation':
|
||||
models.shapeCreator.switchCreateMode(true);
|
||||
break;
|
||||
case 'merge':
|
||||
models.shapeMerger.cancel();
|
||||
break;
|
||||
case 'groupping':
|
||||
models.shapeGrouper.cancel();
|
||||
break;
|
||||
case 'paste':
|
||||
models.shapeBuffer.switchPaste();
|
||||
break;
|
||||
case 'poly_editing':
|
||||
models.shapeEditor.finish();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
Mousetrap.bind(shortkeys.open_help.value, openHelpHandler, 'keydown');
|
||||
Mousetrap.bind(shortkeys.open_settings.value, openSettingsHandler, 'keydown');
|
||||
Mousetrap.bind(shortkeys.cancel_mode.value, cancelModeHandler, 'keydown');
|
||||
}
|
||||
|
||||
|
||||
function setupHelpWindow(shortkeys) {
|
||||
const closeHelpButton = $('#closeHelpButton');
|
||||
const helpTable = $('#shortkeyHelpTable');
|
||||
|
||||
closeHelpButton.on('click', () => {
|
||||
$('#helpWindow').addClass('hidden');
|
||||
});
|
||||
|
||||
for (const key in shortkeys) {
|
||||
if (Object.prototype.hasOwnProperty.call(shortkeys, key)) {
|
||||
helpTable.append($(`<tr> <td> ${shortkeys[key].view_value} </td> <td> ${shortkeys[key].description} </td> </tr>`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function setupSettingsWindow() {
|
||||
const closeSettingsButton = $('#closeSettignsButton');
|
||||
|
||||
closeSettingsButton.on('click', () => {
|
||||
$('#settingsWindow').addClass('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function setupMenu(job, task, shapeCollectionModel,
|
||||
annotationParser, aamModel, playerModel, historyModel,
|
||||
annotationFormats, annotationSaverModel) {
|
||||
const annotationMenu = $('#annotationMenu');
|
||||
const menuButton = $('#menuButton');
|
||||
const downloadDropdownMenu = $('#downloadDropdownMenu');
|
||||
|
||||
function hide() {
|
||||
annotationMenu.addClass('hidden');
|
||||
downloadDropdownMenu.addClass('hidden');
|
||||
}
|
||||
|
||||
function setupVisibility() {
|
||||
let timer = null;
|
||||
menuButton.on('click', () => {
|
||||
const [byLabelsStat, totalStat] = shapeCollectionModel.collectStatistic();
|
||||
const table = $('#annotationStatisticTable');
|
||||
table.find('.temporaryStatisticRow').remove();
|
||||
|
||||
for (const labelId in byLabelsStat) {
|
||||
if (Object.prototype.hasOwnProperty.call(byLabelsStat, labelId)) {
|
||||
$(`<tr>
|
||||
<td class="semiBold"> ${window.cvat.labelsInfo.labels()[labelId].normalize()} </td>
|
||||
<td> ${byLabelsStat[labelId].boxes.annotation} </td>
|
||||
<td> ${byLabelsStat[labelId].boxes.interpolation} </td>
|
||||
<td> ${byLabelsStat[labelId].polygons.annotation} </td>
|
||||
<td> ${byLabelsStat[labelId].polygons.interpolation} </td>
|
||||
<td> ${byLabelsStat[labelId].polylines.annotation} </td>
|
||||
<td> ${byLabelsStat[labelId].polylines.interpolation} </td>
|
||||
<td> ${byLabelsStat[labelId].points.annotation} </td>
|
||||
<td> ${byLabelsStat[labelId].points.interpolation} </td>
|
||||
<td> ${byLabelsStat[labelId].cuboids.annotation} </td>
|
||||
<td> ${byLabelsStat[labelId].cuboids.interpolation} </td>
|
||||
<td> ${byLabelsStat[labelId].manually} </td>
|
||||
<td> ${byLabelsStat[labelId].interpolated} </td>
|
||||
<td class="semiBold"> ${byLabelsStat[labelId].total} </td>
|
||||
</tr>`).addClass('temporaryStatisticRow').appendTo(table);
|
||||
}
|
||||
}
|
||||
|
||||
$(`<tr class="semiBold">
|
||||
<td> Total: </td>
|
||||
<td> ${totalStat.boxes.annotation} </td>
|
||||
<td> ${totalStat.boxes.interpolation} </td>
|
||||
<td> ${totalStat.polygons.annotation} </td>
|
||||
<td> ${totalStat.polygons.interpolation} </td>
|
||||
<td> ${totalStat.polylines.annotation} </td>
|
||||
<td> ${totalStat.polylines.interpolation} </td>
|
||||
<td> ${totalStat.points.annotation} </td>
|
||||
<td> ${totalStat.points.interpolation} </td>
|
||||
<td> ${totalStat.cuboids.annotation} </td>
|
||||
<td> ${totalStat.cuboids.interpolation} </td>
|
||||
<td> ${totalStat.manually} </td>
|
||||
<td> ${totalStat.interpolated} </td>
|
||||
<td> ${totalStat.total} </td>
|
||||
</tr>`).addClass('temporaryStatisticRow').appendTo(table);
|
||||
});
|
||||
|
||||
menuButton.on('click', () => {
|
||||
annotationMenu.removeClass('hidden');
|
||||
annotationMenu.css('top', `${menuButton.offset().top - annotationMenu.height() - menuButton.height()}px`);
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
|
||||
timer = setTimeout(hide, 1000);
|
||||
});
|
||||
|
||||
annotationMenu.on('mouseout', () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
|
||||
timer = setTimeout(hide, 500);
|
||||
});
|
||||
|
||||
annotationMenu.on('mouseover', () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setupVisibility();
|
||||
|
||||
$('#statTaskName').text(task.name);
|
||||
$('#statFrames').text(`[${window.cvat.player.frames.start}-${window.cvat.player.frames.stop}]`);
|
||||
$('#statOverlap').text(task.overlap);
|
||||
$('#statZOrder').text(task.z_order);
|
||||
$('#statTaskStatus').prop('value', job.status).on('change', async (e) => {
|
||||
try {
|
||||
const jobCopy = JSON.parse(JSON.stringify(job));
|
||||
jobCopy.status = e.target.value;
|
||||
|
||||
await $.ajax({
|
||||
url: `/api/v1/jobs/${window.cvat.job.id}`,
|
||||
type: 'PATCH',
|
||||
data: JSON.stringify(jobCopy),
|
||||
contentType: 'application/json',
|
||||
});
|
||||
} catch (errorData) {
|
||||
const message = `Can not update a job status. Code: ${errorData.status}. `
|
||||
+ `Message: ${errorData.responseText || errorData.statusText}`;
|
||||
showMessage(message);
|
||||
}
|
||||
});
|
||||
|
||||
const { shortkeys } = window.cvat.config;
|
||||
$('#helpButton').on('click', () => {
|
||||
hide();
|
||||
$('#helpWindow').removeClass('hidden');
|
||||
});
|
||||
|
||||
$('#helpButton').attr('title', `
|
||||
${shortkeys.open_help.view_value} - ${shortkeys.open_help.description}`);
|
||||
|
||||
$('#settingsButton').on('click', () => {
|
||||
hide();
|
||||
$('#settingsWindow').removeClass('hidden');
|
||||
});
|
||||
|
||||
$('#openTaskButton').on('click', () => {
|
||||
const win = window.open(
|
||||
`${window.UI_URL}/tasks/${window.cvat.job.task_id}`, '_blank'
|
||||
);
|
||||
win.focus();
|
||||
});
|
||||
|
||||
$('#settingsButton').attr('title', `
|
||||
${shortkeys.open_settings.view_value} - ${shortkeys.open_settings.description}`);
|
||||
|
||||
const downloadButton = $('#downloadAnnotationButton');
|
||||
const uploadButton = $('#uploadAnnotationButton');
|
||||
|
||||
const loaders = {};
|
||||
|
||||
for (const dumper of annotationFormats.exporters) {
|
||||
const item = $(`<option>${dumper.name}</li>`);
|
||||
|
||||
if (!isDefaultFormat(dumper.name, window.cvat.job.mode)) {
|
||||
item.addClass('regular');
|
||||
}
|
||||
|
||||
item.appendTo(downloadButton);
|
||||
}
|
||||
|
||||
for (const loader of annotationFormats.importers) {
|
||||
loaders[loader.name] = loader;
|
||||
$(`<option class="regular">${loader.name}</li>`).appendTo(uploadButton);
|
||||
}
|
||||
|
||||
downloadButton.on('change', async (e) => {
|
||||
const dumper = e.target.value;
|
||||
downloadButton.prop('value', 'Dump Annotation');
|
||||
try {
|
||||
downloadButton.prop('disabled', true);
|
||||
await dumpAnnotationRequest(task.id, dumper);
|
||||
} catch (error) {
|
||||
showMessage(error.message);
|
||||
} finally {
|
||||
downloadButton.prop('disabled', false);
|
||||
}
|
||||
});
|
||||
|
||||
uploadButton.on('change', (e) => {
|
||||
const loader = loaders[e.target.value];
|
||||
uploadButton.prop('value', 'Upload Annotation');
|
||||
userConfirm('Current annotation will be removed from the client. Continue?',
|
||||
async () => {
|
||||
try {
|
||||
await uploadAnnotation(
|
||||
job.id,
|
||||
shapeCollectionModel,
|
||||
historyModel,
|
||||
annotationSaverModel,
|
||||
$('#uploadAnnotationButton'),
|
||||
loader,
|
||||
);
|
||||
} catch (error) {
|
||||
showMessage(error.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#removeAnnotationButton').on('click', () => {
|
||||
if (!window.cvat.mode) {
|
||||
hide();
|
||||
userConfirm('Do you want to remove all annotations? The action cannot be undone!',
|
||||
() => {
|
||||
historyModel.empty();
|
||||
shapeCollectionModel.empty();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// JS function cancelFullScreen don't work after pressing
|
||||
// and it is famous problem.
|
||||
$('#fullScreenButton').on('click', () => {
|
||||
$('#playerFrame').toggleFullScreen();
|
||||
});
|
||||
|
||||
$('#playerFrame').on('fullscreenchange webkitfullscreenchange mozfullscreenchange', () => {
|
||||
playerModel.updateGeometry({
|
||||
width: $('#playerFrame').width(),
|
||||
height: $('#playerFrame').height(),
|
||||
});
|
||||
playerModel.fit();
|
||||
});
|
||||
|
||||
$('#switchAAMButton').on('click', () => {
|
||||
hide();
|
||||
aamModel.switchAAMMode();
|
||||
});
|
||||
|
||||
$('#switchAAMButton').attr('title', `
|
||||
${shortkeys.switch_aam_mode.view_value} - ${shortkeys.switch_aam_mode.description}`);
|
||||
}
|
||||
|
||||
|
||||
function buildAnnotationUI(
|
||||
jobData, taskData, imageMetaData,
|
||||
annotationData, annotationFormats, loadJobEvent,
|
||||
) {
|
||||
// Setup some API
|
||||
window.cvat = {
|
||||
labelsInfo: new LabelsInfo(taskData.labels),
|
||||
translate: new CoordinateTranslator(),
|
||||
frozen: true,
|
||||
player: {
|
||||
geometry: {
|
||||
scale: 1,
|
||||
},
|
||||
frames: {
|
||||
current: jobData.start_frame,
|
||||
start: jobData.start_frame,
|
||||
stop: jobData.stop_frame,
|
||||
},
|
||||
},
|
||||
mode: null,
|
||||
job: {
|
||||
z_order: taskData.z_order,
|
||||
id: jobData.id,
|
||||
task_id: taskData.id,
|
||||
mode: taskData.mode,
|
||||
images: imageMetaData,
|
||||
chunk_size: taskData.data_chunk_size,
|
||||
},
|
||||
search: {
|
||||
value: window.location.search,
|
||||
|
||||
set(name, value) {
|
||||
const searchParams = new URLSearchParams(this.value);
|
||||
|
||||
if (typeof value === 'undefined' || value === null) {
|
||||
if (searchParams.has(name)) {
|
||||
searchParams.delete(name);
|
||||
}
|
||||
} else {
|
||||
searchParams.set(name, value);
|
||||
}
|
||||
this.value = `${searchParams.toString()}`;
|
||||
},
|
||||
|
||||
get(name) {
|
||||
try {
|
||||
const decodedURI = decodeURIComponent(this.value);
|
||||
const urlSearchParams = new URLSearchParams(decodedURI);
|
||||
if (urlSearchParams.has(name)) {
|
||||
return urlSearchParams.get(name);
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
showMessage('Bad URL has been received');
|
||||
this.value = window.location.href;
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
toString() {
|
||||
return `${window.location.origin}/?${this.value}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Remove external search parameters from url
|
||||
window.history.replaceState(null, null, `${window.location.origin}/?id=${jobData.id}`);
|
||||
|
||||
window.cvat.config = new Config();
|
||||
|
||||
// Setup components
|
||||
const annotationParser = new AnnotationParser({
|
||||
start: window.cvat.player.frames.start,
|
||||
stop: window.cvat.player.frames.stop,
|
||||
flipped: taskData.flipped,
|
||||
image_meta_data: imageMetaData,
|
||||
}, window.cvat.labelsInfo);
|
||||
|
||||
const shapeCollectionModel = new ShapeCollectionModel().import(annotationData);
|
||||
const shapeCollectionController = new ShapeCollectionController(shapeCollectionModel);
|
||||
const shapeCollectionView = new ShapeCollectionView(shapeCollectionModel,
|
||||
shapeCollectionController);
|
||||
|
||||
const annotationSaverModel = buildAnnotationSaver(annotationData, shapeCollectionModel);
|
||||
|
||||
window.cvat.data = {
|
||||
get: () => shapeCollectionModel.export()[0],
|
||||
set: (data) => {
|
||||
shapeCollectionModel.import(data);
|
||||
shapeCollectionModel.update();
|
||||
},
|
||||
clear: () => shapeCollectionModel.empty(),
|
||||
};
|
||||
|
||||
const shapeBufferModel = new ShapeBufferModel(shapeCollectionModel);
|
||||
const shapeBufferController = new ShapeBufferController(shapeBufferModel);
|
||||
const shapeBufferView = new ShapeBufferView(shapeBufferModel, shapeBufferController);
|
||||
|
||||
$('#shapeModeSelector').prop('value', taskData.mode);
|
||||
const shapeCreatorModel = new ShapeCreatorModel(shapeCollectionModel);
|
||||
const shapeCreatorController = new ShapeCreatorController(shapeCreatorModel);
|
||||
const shapeCreatorView = new ShapeCreatorView(shapeCreatorModel, shapeCreatorController);
|
||||
|
||||
const polyshapeEditorModel = new PolyshapeEditorModel(shapeCollectionModel);
|
||||
const polyshapeEditorController = new PolyshapeEditorController(polyshapeEditorModel);
|
||||
const polyshapeEditorView = new PolyshapeEditorView(polyshapeEditorModel,
|
||||
polyshapeEditorController);
|
||||
|
||||
// Add static member for class. It will be used by all polyshapes.
|
||||
PolyShapeView.editor = polyshapeEditorModel;
|
||||
|
||||
const shapeMergerModel = new ShapeMergerModel(shapeCollectionModel);
|
||||
const shapeMergerController = new ShapeMergerController(shapeMergerModel);
|
||||
new ShapeMergerView(shapeMergerModel, shapeMergerController);
|
||||
|
||||
const shapeGrouperModel = new ShapeGrouperModel(shapeCollectionModel);
|
||||
const shapeGrouperController = new ShapeGrouperController(shapeGrouperModel);
|
||||
const shapeGrouperView = new ShapeGrouperView(shapeGrouperModel, shapeGrouperController);
|
||||
|
||||
const playerGeometry = {
|
||||
width: $('#playerFrame').width(),
|
||||
height: $('#playerFrame').height(),
|
||||
};
|
||||
|
||||
const playerModel = new PlayerModel(taskData, playerGeometry);
|
||||
const playerController = new PlayerController(playerModel,
|
||||
() => shapeCollectionModel.activeShape,
|
||||
direction => shapeCollectionModel.find(direction),
|
||||
Object.assign({}, playerGeometry, {
|
||||
left: $('#playerFrame').offset().left,
|
||||
top: $('#playerFrame').offset().top,
|
||||
}));
|
||||
new PlayerView(playerModel, playerController);
|
||||
|
||||
|
||||
const aamModel = new AAMModel(shapeCollectionModel, (xtl, xbr, ytl, ybr) => {
|
||||
playerModel.focus(xtl, xbr, ytl, ybr);
|
||||
}, () => {
|
||||
playerModel.fit();
|
||||
});
|
||||
const aamController = new AAMController(aamModel);
|
||||
new AAMView(aamModel, aamController);
|
||||
|
||||
shapeCreatorModel.subscribe(shapeCollectionModel);
|
||||
shapeGrouperModel.subscribe(shapeCollectionView);
|
||||
shapeCollectionModel.subscribe(shapeGrouperModel);
|
||||
|
||||
$('#playerProgress').css('width', $('#player')['0'].clientWidth - 420);
|
||||
|
||||
const historyModel = new HistoryModel(playerModel);
|
||||
const historyController = new HistoryController(historyModel);
|
||||
new HistoryView(historyController, historyModel);
|
||||
|
||||
playerModel.subscribe(shapeCollectionModel);
|
||||
playerModel.subscribe(shapeCollectionView);
|
||||
playerModel.subscribe(shapeCreatorView);
|
||||
playerModel.subscribe(shapeBufferView);
|
||||
playerModel.subscribe(shapeGrouperView);
|
||||
playerModel.subscribe(polyshapeEditorView);
|
||||
playerModel.shift(window.cvat.search.get('frame') || 0, true);
|
||||
|
||||
const { shortkeys } = window.cvat.config;
|
||||
setupHelpWindow(shortkeys);
|
||||
setupSettingsWindow();
|
||||
setupMenu(jobData, taskData, shapeCollectionModel,
|
||||
annotationParser, aamModel, playerModel, historyModel,
|
||||
annotationFormats, annotationSaverModel);
|
||||
setupFrameFilters();
|
||||
setupShortkeys(shortkeys, {
|
||||
aam: aamModel,
|
||||
shapeCreator: shapeCreatorModel,
|
||||
shapeMerger: shapeMergerModel,
|
||||
shapeGrouper: shapeGrouperModel,
|
||||
shapeBuffer: shapeBufferModel,
|
||||
shapeEditor: polyshapeEditorModel,
|
||||
});
|
||||
|
||||
$(window).on('click', (event) => {
|
||||
Logger.updateUserActivityTimer();
|
||||
if (event.target.classList.contains('modal') && !event.target.classList.contains('force-modal')) {
|
||||
event.target.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
const totalStat = shapeCollectionModel.collectStatistic()[1];
|
||||
loadJobEvent.addValues({
|
||||
'track count': totalStat.boxes.annotation + totalStat.boxes.interpolation
|
||||
+ totalStat.polygons.annotation + totalStat.polygons.interpolation
|
||||
+ totalStat.polylines.annotation + totalStat.polylines.interpolation
|
||||
+ totalStat.points.annotation + totalStat.points.interpolation
|
||||
+ totalStat.cuboids.annotation + totalStat.cuboids.interpolation,
|
||||
'frame count': window.cvat.player.frames.stop - window.cvat.player.frames.start + 1,
|
||||
'object count': totalStat.total,
|
||||
'box count': totalStat.boxes.annotation + totalStat.boxes.interpolation,
|
||||
'polygon count': totalStat.polygons.annotation + totalStat.polygons.interpolation,
|
||||
'polyline count': totalStat.polylines.annotation + totalStat.polylines.interpolation,
|
||||
'points count': totalStat.points.annotation + totalStat.points.interpolation,
|
||||
'cuboid count': totalStat.cuboids.annotation + totalStat.cuboids.interpolation,
|
||||
});
|
||||
loadJobEvent.close();
|
||||
|
||||
$('#player').on('click', (e) => {
|
||||
if (e.target.tagName.toLowerCase() !== 'input') {
|
||||
blurAllElements();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function callAnnotationUI(jid) {
|
||||
function onError(errorData) {
|
||||
$('body').empty();
|
||||
const message = `Can not build CVAT annotation UI. Code: ${errorData.status}. `
|
||||
+ `Message: ${errorData.responseText || errorData.statusText}`;
|
||||
showMessage(message);
|
||||
}
|
||||
|
||||
initLogger(jid);
|
||||
|
||||
const loadJobEvent = Logger.addContinuedEvent(Logger.EventType.loadJob);
|
||||
$.get(`/api/v1/jobs/${jid}`).done((jobData) => {
|
||||
$.when(
|
||||
$.get(`/api/v1/tasks/${jobData.task_id}`),
|
||||
$.get(`/api/v1/tasks/${jobData.task_id}/data/meta`),
|
||||
$.get(`/api/v1/jobs/${jid}/annotations`),
|
||||
$.get('/api/v1/server/annotation/formats'),
|
||||
).then((taskData, imageMetaData, annotationData, annotationFormats) => {
|
||||
$('#loadingOverlay').remove();
|
||||
setTimeout(async () => {
|
||||
window.cvat.config.backendAPI = `${window.location.origin}/api/v1`;
|
||||
[window.cvatTask] = (await window.cvat.tasks.get({ id: taskData[0].id }));
|
||||
buildAnnotationUI(jobData, taskData[0],
|
||||
imageMetaData[0], annotationData[0], annotationFormats[0], loadJobEvent);
|
||||
});
|
||||
}).fail(onError);
|
||||
}).fail(onError);
|
||||
}
|
||||
|
||||
|
||||
function copyToClipboard(text) {
|
||||
const tempInput = $('<input>');
|
||||
$('body').append(tempInput);
|
||||
tempInput.prop('value', text).select();
|
||||
document.execCommand('copy');
|
||||
tempInput.remove();
|
||||
}
|
||||
|
||||
|
||||
function drawBoxSize(boxScene, textScene, box) {
|
||||
const clientBox = window.cvat.translate.box.canvasToClient(boxScene.node, box);
|
||||
const text = `${box.width.toFixed(1)}x${box.height.toFixed(1)}`;
|
||||
const obj = this && this.textUI && this.rm ? this : {
|
||||
textUI: textScene.text('').font({
|
||||
weight: 'bolder',
|
||||
}).fill('white'),
|
||||
|
||||
rm() {
|
||||
if (this.textUI) {
|
||||
this.textUI.remove();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const textPoint = window.cvat.translate.point.clientToCanvas(textScene.node,
|
||||
clientBox.x, clientBox.y);
|
||||
|
||||
obj.textUI.clear().plain(text);
|
||||
obj.textUI.addClass('shapeText');
|
||||
obj.textUI.move(textPoint.x, textPoint.y);
|
||||
|
||||
return obj;
|
||||
}
|
||||
@ -1,453 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* exported AAMModel AAMController AAMView AAMUndefinedKeyword */
|
||||
|
||||
/* global
|
||||
Listener:false
|
||||
Logger:false
|
||||
Mousetrap:false
|
||||
PolyShapeModel:false
|
||||
SVG:false
|
||||
*/
|
||||
|
||||
const AAMUndefinedKeyword = '__undefined__';
|
||||
|
||||
class AAMModel extends Listener {
|
||||
constructor(shapeCollection, focus, fit) {
|
||||
super('onAAMUpdate', () => this);
|
||||
this._shapeCollection = shapeCollection;
|
||||
this._focus = focus;
|
||||
this._fit = fit;
|
||||
this._activeAAM = false;
|
||||
this._activeIdx = null;
|
||||
this._active = null;
|
||||
this._margin = 100;
|
||||
this._currentShapes = [];
|
||||
this._attrNumberByLabel = {};
|
||||
this._helps = {};
|
||||
|
||||
function getHelp(attrId) {
|
||||
const attrInfo = window.cvat.labelsInfo.attrInfo(attrId);
|
||||
const help = [];
|
||||
switch (attrInfo.type) {
|
||||
case 'checkbox':
|
||||
help.push(`0 - ${attrInfo.values[0]}`);
|
||||
help.push(`1 - ${!attrInfo.values[0]}`);
|
||||
break;
|
||||
default:
|
||||
for (let idx = 0; idx < attrInfo.values.length; idx += 1) {
|
||||
if (idx > 9) {
|
||||
break;
|
||||
}
|
||||
if (attrInfo.values[0] === AAMUndefinedKeyword) {
|
||||
if (!idx) {
|
||||
continue;
|
||||
}
|
||||
help.push(`${idx - 1} - ${attrInfo.values[idx]}`);
|
||||
} else {
|
||||
help.push(`${idx} - ${attrInfo.values[idx]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return help;
|
||||
}
|
||||
|
||||
const labels = window.cvat.labelsInfo.labels();
|
||||
for (const labelId in labels) {
|
||||
if (Object.prototype.hasOwnProperty.call(labels, labelId)) {
|
||||
const labelAttributes = window.cvat.labelsInfo.labelAttributes(labelId);
|
||||
if (Object.keys(labelAttributes).length) {
|
||||
this._attrNumberByLabel[labelId] = {
|
||||
current: 0,
|
||||
end: Object.keys(labelAttributes).length,
|
||||
};
|
||||
|
||||
for (const attrId in labelAttributes) {
|
||||
if (Object.prototype.hasOwnProperty.call(labelAttributes, attrId)) {
|
||||
this._helps[attrId] = {
|
||||
title: `${window.cvat.labelsInfo.labels()[labelId]}, ${window.cvat.labelsInfo.attributes()[attrId]}`,
|
||||
help: getHelp(attrId),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shapeCollection.subscribe(this);
|
||||
}
|
||||
|
||||
_bbRect(pos) {
|
||||
if ('points' in pos) {
|
||||
const points = PolyShapeModel.convertStringToNumberArray(pos.points);
|
||||
let xtl = Number.MAX_SAFE_INTEGER;
|
||||
let ytl = Number.MAX_SAFE_INTEGER;
|
||||
let xbr = Number.MIN_SAFE_INTEGER;
|
||||
let ybr = Number.MIN_SAFE_INTEGER;
|
||||
for (const point of points) {
|
||||
xtl = Math.min(xtl, point.x);
|
||||
ytl = Math.min(ytl, point.y);
|
||||
xbr = Math.max(xbr, point.x);
|
||||
ybr = Math.max(ybr, point.y);
|
||||
}
|
||||
return [xtl, ytl, xbr, ybr];
|
||||
}
|
||||
return [pos.xtl, pos.ytl, pos.xbr, pos.ybr];
|
||||
}
|
||||
|
||||
_updateCollection() {
|
||||
this._currentShapes = [];
|
||||
|
||||
for (const shape of this._shapeCollection.currentShapes) {
|
||||
const labelAttributes = window.cvat.labelsInfo.labelAttributes(shape.model.label);
|
||||
if (Object.keys(labelAttributes).length
|
||||
&& !shape.model.removed && !shape.interpolation.position.outside) {
|
||||
this._currentShapes.push({
|
||||
model: shape.model,
|
||||
interpolation: shape.model.interpolate(window.cvat.player.frames.current),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (this._currentShapes.length) {
|
||||
this._activeIdx = 0;
|
||||
this._active = this._currentShapes[0].model;
|
||||
} else {
|
||||
this._activeIdx = null;
|
||||
this._active = null;
|
||||
}
|
||||
}
|
||||
|
||||
_attrIdByIdx(labelId, attrIdx) {
|
||||
return Object.keys(window.cvat.labelsInfo.labelAttributes(labelId))[attrIdx];
|
||||
}
|
||||
|
||||
_activate() {
|
||||
if (this._activeAAM && this._active) {
|
||||
const { label } = this._active;
|
||||
|
||||
|
||||
const [xtl, ytl, xbr, ybr] = this._bbRect(this._currentShapes[this._activeIdx]
|
||||
.interpolation.position);
|
||||
this._focus(xtl - this._margin, xbr + this._margin,
|
||||
ytl - this._margin, ybr + this._margin);
|
||||
this.notify();
|
||||
|
||||
if (typeof (this._attrNumberByLabel[label]) !== 'undefined') {
|
||||
const attrId = +this._attrIdByIdx(label, this._attrNumberByLabel[label].current);
|
||||
this._active.activeAttribute = attrId;
|
||||
}
|
||||
} else {
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
|
||||
_deactivate() {
|
||||
if (this._activeAAM && this._active) {
|
||||
this._active.activeAttribute = null;
|
||||
}
|
||||
}
|
||||
|
||||
_enable() {
|
||||
if (window.cvat.mode === null) {
|
||||
window.cvat.mode = 'aam';
|
||||
this._shapeCollection.resetActive();
|
||||
this._activeAAM = true;
|
||||
this._updateCollection();
|
||||
this._activate();
|
||||
}
|
||||
}
|
||||
|
||||
_disable() {
|
||||
if (this._activeAAM && window.cvat.mode === 'aam') {
|
||||
this._deactivate();
|
||||
window.cvat.mode = null;
|
||||
this._activeAAM = false;
|
||||
this._activeIdx = null;
|
||||
this._active = null;
|
||||
|
||||
// Notify for remove aam UI
|
||||
this.notify();
|
||||
this._fit();
|
||||
}
|
||||
}
|
||||
|
||||
switchAAMMode() {
|
||||
if (this._activeAAM) {
|
||||
this._disable();
|
||||
} else {
|
||||
this._enable();
|
||||
}
|
||||
}
|
||||
|
||||
moveShape(direction) {
|
||||
if (!this._activeAAM || this._currentShapes.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._deactivate();
|
||||
if (Math.sign(direction) < 0) {
|
||||
// next
|
||||
this._activeIdx += 1;
|
||||
if (this._activeIdx >= this._currentShapes.length) {
|
||||
this._activeIdx = 0;
|
||||
}
|
||||
} else {
|
||||
// prev
|
||||
this._activeIdx -= 1;
|
||||
if (this._activeIdx < 0) {
|
||||
this._activeIdx = this._currentShapes.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
this._active = this._currentShapes[this._activeIdx].model;
|
||||
this._activate();
|
||||
}
|
||||
|
||||
moveAttr(direction) {
|
||||
if (!this._activeAAM || !this._active) {
|
||||
return;
|
||||
}
|
||||
|
||||
const curAttr = this._attrNumberByLabel[this._active.label];
|
||||
if (typeof (curAttr) === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (curAttr.end < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Math.sign(direction) > 0) {
|
||||
// next
|
||||
curAttr.current += 1;
|
||||
if (curAttr.current >= curAttr.end) {
|
||||
curAttr.current = 0;
|
||||
}
|
||||
} else {
|
||||
// prev
|
||||
curAttr.current -= 1;
|
||||
if (curAttr.current < 0) {
|
||||
curAttr.current = curAttr.end - 1;
|
||||
}
|
||||
}
|
||||
this._activate();
|
||||
}
|
||||
|
||||
setupAttributeValue(key) {
|
||||
if (!this._activeAAM || !this._active) {
|
||||
return;
|
||||
}
|
||||
const { label } = this._active;
|
||||
const frame = window.cvat.player.frames.current;
|
||||
if (typeof (this._attrNumberByLabel[label]) === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const attrId = this._attrIdByIdx(label, this._attrNumberByLabel[label].current);
|
||||
const attrInfo = window.cvat.labelsInfo.attrInfo(attrId);
|
||||
if (key >= attrInfo.values.length) {
|
||||
if (attrInfo.type === 'checkbox' && key < 2) {
|
||||
this._active.updateAttribute(frame, attrId, !attrInfo.values[0]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (attrInfo.values[0] === AAMUndefinedKeyword) {
|
||||
if (key >= attrInfo.values.length - 1) {
|
||||
return;
|
||||
}
|
||||
key += 1;
|
||||
}
|
||||
this._active.updateAttribute(frame, attrId, attrInfo.values[key]);
|
||||
}
|
||||
|
||||
onCollectionUpdate() {
|
||||
if (this._activeAAM) {
|
||||
// No need deactivate active view because all listeners already unsubscribed
|
||||
this._updateCollection();
|
||||
this._activate();
|
||||
}
|
||||
}
|
||||
|
||||
generateHelps() {
|
||||
if (this._active) {
|
||||
const { label } = this._active;
|
||||
if (typeof (this._attrNumberByLabel[label]) !== 'undefined') {
|
||||
const attrId = +this._attrIdByIdx(label, this._attrNumberByLabel[label].current);
|
||||
return [this._helps[attrId].title, this._helps[attrId].help, `${this._activeIdx + 1}/${this._currentShapes.length}`];
|
||||
}
|
||||
return ['No Attributes Found', '', `${this._activeIdx + 1}/${this._currentShapes.length}`];
|
||||
}
|
||||
return ['No Shapes Found', '', '0/0'];
|
||||
}
|
||||
|
||||
get activeAAM() {
|
||||
return this._activeAAM;
|
||||
}
|
||||
|
||||
get active() {
|
||||
return this._active;
|
||||
}
|
||||
|
||||
set margin(value) {
|
||||
this._margin = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AAMController {
|
||||
constructor(aamModel) {
|
||||
this._model = aamModel;
|
||||
|
||||
function setupAAMShortkeys() {
|
||||
const switchAAMHandler = Logger.shortkeyLogDecorator(() => {
|
||||
this._model.switchAAMMode();
|
||||
});
|
||||
|
||||
const nextAttributeHandler = Logger.shortkeyLogDecorator((e) => {
|
||||
this._model.moveAttr(1);
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
const prevAttributeHandler = Logger.shortkeyLogDecorator((e) => {
|
||||
this._model.moveAttr(-1);
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
const nextShapeHandler = Logger.shortkeyLogDecorator((e) => {
|
||||
this._model.moveShape(1);
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
const prevShapeHandler = Logger.shortkeyLogDecorator((e) => {
|
||||
this._model.moveShape(-1);
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
const selectAttributeHandler = Logger.shortkeyLogDecorator((e) => {
|
||||
let key = e.keyCode;
|
||||
if (key >= 48 && key <= 57) {
|
||||
key -= 48; // 0 and 9
|
||||
} else if (key >= 96 && key <= 105) {
|
||||
key -= 96; // num 0 and 9
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
this._model.setupAttributeValue(key);
|
||||
});
|
||||
|
||||
const { shortkeys } = window.cvat.config;
|
||||
Mousetrap.bind(shortkeys.switch_aam_mode.value, switchAAMHandler, 'keydown');
|
||||
Mousetrap.bind(shortkeys.aam_next_attribute.value, nextAttributeHandler, 'keydown');
|
||||
Mousetrap.bind(shortkeys.aam_prev_attribute.value, prevAttributeHandler, 'keydown');
|
||||
Mousetrap.bind(shortkeys.aam_next_shape.value, nextShapeHandler, 'keydown');
|
||||
Mousetrap.bind(shortkeys.aam_prev_shape.value, prevShapeHandler, 'keydown');
|
||||
Mousetrap.bind(shortkeys.select_i_attribute.value, selectAttributeHandler, 'keydown');
|
||||
}
|
||||
|
||||
setupAAMShortkeys.call(this);
|
||||
}
|
||||
|
||||
setMargin(value) {
|
||||
this._model.margin = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AAMView {
|
||||
constructor(aamModel, aamController) {
|
||||
this._trackManagement = $('#trackManagement');
|
||||
this._aamMenu = $('#aamMenu');
|
||||
this._aamTitle = $('#aamTitle');
|
||||
this._aamCounter = $('#aamCounter');
|
||||
this._aamHelpContainer = $('#aamHelpContainer');
|
||||
this._zoomMargin = $('#aamZoomMargin');
|
||||
this._frameContent = SVG.adopt($('#frameContent')[0]);
|
||||
this._controller = aamController;
|
||||
|
||||
this._zoomMargin.on('change', (e) => {
|
||||
const value = +e.target.value;
|
||||
this._controller.setMargin(value);
|
||||
}).trigger('change');
|
||||
aamModel.subscribe(this);
|
||||
}
|
||||
|
||||
_setupAAMView(active, type, pos) {
|
||||
const oldRect = $('#outsideRect');
|
||||
const oldMask = $('#outsideMask');
|
||||
|
||||
if (active) {
|
||||
if (oldRect.length) {
|
||||
oldRect.remove();
|
||||
oldMask.remove();
|
||||
}
|
||||
|
||||
const size = window.cvat.translate.box.actualToCanvas({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: window.cvat.player.geometry.frameWidth,
|
||||
height: window.cvat.player.geometry.frameHeight,
|
||||
});
|
||||
|
||||
const excludeField = this._frameContent.rect(size.width, size.height).move(size.x, size.y).fill('#666');
|
||||
let includeField = null;
|
||||
|
||||
if (type === 'box') {
|
||||
pos = window.cvat.translate.box.actualToCanvas(pos);
|
||||
includeField = this._frameContent.rect(pos.xbr - pos.xtl,
|
||||
pos.ybr - pos.ytl).move(pos.xtl, pos.ytl);
|
||||
} else {
|
||||
pos.points = window.cvat.translate.points.actualToCanvas(pos.points);
|
||||
includeField = this._frameContent.polygon(pos.points);
|
||||
}
|
||||
|
||||
this._frameContent.mask().add(excludeField)
|
||||
.add(includeField).fill('black')
|
||||
.attr('id', 'outsideMask');
|
||||
this._frameContent.rect(size.width, size.height)
|
||||
.move(size.x, size.y).attr({
|
||||
mask: 'url(#outsideMask)',
|
||||
id: 'outsideRect',
|
||||
});
|
||||
|
||||
const content = $(this._frameContent.node);
|
||||
const texts = content.find('.shapeText');
|
||||
for (const text of texts) {
|
||||
content.append(text);
|
||||
}
|
||||
} else {
|
||||
oldRect.remove();
|
||||
oldMask.remove();
|
||||
}
|
||||
}
|
||||
|
||||
onAAMUpdate(aam) {
|
||||
this._setupAAMView(Boolean(aam.active),
|
||||
aam.active ? aam.active.type.split('_')[1] : '',
|
||||
aam.active ? aam.active.interpolate(window.cvat.player.frames.current).position : 0);
|
||||
|
||||
if (aam.activeAAM) {
|
||||
if (this._aamMenu.hasClass('hidden')) {
|
||||
this._trackManagement.addClass('hidden');
|
||||
this._aamMenu.removeClass('hidden');
|
||||
}
|
||||
|
||||
const [title, help, counter] = aam.generateHelps();
|
||||
this._aamHelpContainer.empty();
|
||||
this._aamCounter.text(counter);
|
||||
this._aamTitle.text(title);
|
||||
|
||||
for (const helpRow of help) {
|
||||
$(`<label> ${helpRow} <label> <br>`).appendTo(this._aamHelpContainer);
|
||||
}
|
||||
} else if (this._trackManagement.hasClass('hidden')) {
|
||||
this._aamMenu.addClass('hidden');
|
||||
this._trackManagement.removeClass('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,227 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* exported
|
||||
userConfirm
|
||||
dumpAnnotationRequest
|
||||
showMessage
|
||||
showOverlay
|
||||
uploadJobAnnotationRequest
|
||||
uploadTaskAnnotationRequest
|
||||
isDefaultFormat
|
||||
*/
|
||||
|
||||
/* global
|
||||
Cookies:false
|
||||
*/
|
||||
|
||||
|
||||
Math.clamp = (x, min, max) => Math.min(Math.max(x, min), max);
|
||||
|
||||
String.customSplit = (string, separator) => {
|
||||
const regex = /"/gi;
|
||||
const occurences = [];
|
||||
let occurence = regex.exec(string);
|
||||
while (occurence) {
|
||||
occurences.push(occurence.index);
|
||||
occurence = regex.exec(string);
|
||||
}
|
||||
|
||||
if (occurences.length % 2) {
|
||||
occurences.pop();
|
||||
}
|
||||
|
||||
let copy = '';
|
||||
if (occurences.length) {
|
||||
let start = 0;
|
||||
for (let idx = 0; idx < occurences.length; idx += 2) {
|
||||
copy += string.substr(start, occurences[idx] - start);
|
||||
copy += string.substr(occurences[idx], occurences[idx + 1] - occurences[idx] + 1)
|
||||
.replace(new RegExp(separator, 'g'), '\0');
|
||||
start = occurences[idx + 1] + 1;
|
||||
}
|
||||
copy += string.substr(occurences[occurences.length - 1] + 1);
|
||||
} else {
|
||||
copy = string;
|
||||
}
|
||||
|
||||
return copy.split(new RegExp(separator, 'g')).map(x => x.replace(/\0/g, separator));
|
||||
};
|
||||
|
||||
|
||||
function userConfirm(message, onagree, ondisagree) {
|
||||
const template = $('#confirmTemplate');
|
||||
const confirmWindow = $(template.html()).css('display', 'block');
|
||||
|
||||
const annotationConfirmMessage = confirmWindow.find('.templateMessage');
|
||||
const agreeConfirm = confirmWindow.find('.templateAgreeButton');
|
||||
const disagreeConfirm = confirmWindow.find('.templateDisagreeButton');
|
||||
|
||||
function hideConfirm() {
|
||||
agreeConfirm.off('click');
|
||||
disagreeConfirm.off('click');
|
||||
confirmWindow.remove();
|
||||
}
|
||||
|
||||
annotationConfirmMessage.text(message);
|
||||
$('body').append(confirmWindow);
|
||||
|
||||
agreeConfirm.on('click', () => {
|
||||
hideConfirm();
|
||||
if (onagree) {
|
||||
onagree();
|
||||
}
|
||||
});
|
||||
|
||||
disagreeConfirm.on('click', () => {
|
||||
hideConfirm();
|
||||
if (ondisagree) {
|
||||
ondisagree();
|
||||
}
|
||||
});
|
||||
|
||||
disagreeConfirm.focus();
|
||||
confirmWindow.on('keydown', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function showMessage(message) {
|
||||
const template = $('#messageTemplate');
|
||||
const messageWindow = $(template.html()).css('display', 'block');
|
||||
|
||||
const messageText = messageWindow.find('.templateMessage');
|
||||
const okButton = messageWindow.find('.templateOKButton');
|
||||
|
||||
messageText.text(message);
|
||||
$('body').append(messageWindow);
|
||||
|
||||
messageWindow.on('keydown', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
okButton.on('click', () => {
|
||||
okButton.off('click');
|
||||
messageWindow.remove();
|
||||
});
|
||||
|
||||
okButton.focus();
|
||||
return messageWindow;
|
||||
}
|
||||
|
||||
|
||||
function showOverlay(message) {
|
||||
const template = $('#overlayTemplate');
|
||||
const overlayWindow = $(template.html()).css('display', 'block');
|
||||
const overlayText = overlayWindow.find('.templateMessage');
|
||||
|
||||
overlayWindow[0].getMessage = () => overlayText.html();
|
||||
overlayWindow[0].remove = () => overlayWindow.remove();
|
||||
overlayWindow[0].setMessage = (msg) => {
|
||||
overlayText.html(msg);
|
||||
};
|
||||
|
||||
$('body').append(overlayWindow);
|
||||
overlayWindow[0].setMessage(message);
|
||||
return overlayWindow[0];
|
||||
}
|
||||
|
||||
async function dumpAnnotationRequest(tid, format) {
|
||||
// URL Router on the server doesn't work correctly with slashes.
|
||||
// So, we have to replace them on the client side
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `/api/v1/tasks/${tid}/annotations`;
|
||||
let queryString = `format=${encodeURIComponent(format)}`;
|
||||
|
||||
async function request() {
|
||||
$.get(`${url}?${queryString}`)
|
||||
.done((...args) => {
|
||||
if (args[2].status === 202) {
|
||||
setTimeout(request, 3000);
|
||||
} else {
|
||||
const a = document.createElement('a');
|
||||
queryString = `${queryString}&action=download`;
|
||||
a.href = `${url}?${queryString}`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
resolve();
|
||||
}
|
||||
}).fail((errorData) => {
|
||||
const message = `Can not dump annotations for the task. Code: ${errorData.status}. `
|
||||
+ `Message: ${errorData.responseText || errorData.statusText}`;
|
||||
reject(new Error(message));
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(request);
|
||||
});
|
||||
}
|
||||
|
||||
async function uploadAnnoRequest(url, formData, format) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const queryString = `format=${format}`;
|
||||
async function request(data) {
|
||||
try {
|
||||
await $.ajax({
|
||||
url: `${url}?${queryString}`,
|
||||
type: 'PUT',
|
||||
data,
|
||||
contentType: false,
|
||||
processData: false,
|
||||
}).done((...args) => {
|
||||
if (args[2].status === 202) {
|
||||
setTimeout(() => request(''), 3000);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
} catch (errorData) {
|
||||
const message = `Can not upload annotations for the job. Code: ${errorData.status}. `
|
||||
+ `Message: ${errorData.responseText || errorData.statusText}`;
|
||||
reject(new Error(message));
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => request(formData));
|
||||
});
|
||||
}
|
||||
|
||||
async function uploadJobAnnotationRequest(jid, formData, format) {
|
||||
return uploadAnnoRequest(`/api/v1/jobs/${jid}/annotations`, formData, format);
|
||||
}
|
||||
|
||||
async function uploadTaskAnnotationRequest(tid, formData, format) {
|
||||
return uploadAnnoRequest(`/api/v1/tasks/${tid}/annotations`, formData, format);
|
||||
}
|
||||
|
||||
/* These HTTP methods do not require CSRF protection */
|
||||
function csrfSafeMethod(method) {
|
||||
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
|
||||
}
|
||||
|
||||
|
||||
$.ajaxSetup({
|
||||
beforeSend(xhr, settings) {
|
||||
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
|
||||
xhr.setRequestHeader('X-CSRFToken', Cookies.get('csrftoken'));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
$(document).ready(() => {
|
||||
$('body').css({
|
||||
width: `${window.screen.width}px`,
|
||||
height: `${window.screen.height * 0.95}px`,
|
||||
});
|
||||
});
|
||||
|
||||
function isDefaultFormat(dumperName, taskMode) {
|
||||
return (dumperName === 'CVAT for video 1.1' && taskMode === 'interpolation')
|
||||
|| (dumperName === 'CVAT for images 1.1' && taskMode === 'annotation');
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
callAnnotationUI:false
|
||||
Logger:false
|
||||
platform:false
|
||||
*/
|
||||
|
||||
String.normalize = () => {
|
||||
let target = this;
|
||||
target = target.charAt(0).toUpperCase() + target.substr(1);
|
||||
return target;
|
||||
};
|
||||
|
||||
window.onload = function boot() {
|
||||
window.onerror = function exception(errorMsg, url, lineNumber, colNumber, error) {
|
||||
Logger.sendException(
|
||||
errorMsg,
|
||||
url,
|
||||
lineNumber,
|
||||
colNumber ? String(colNumber) : '',
|
||||
error && error.stack ? error.stack : '',
|
||||
`${platform.name} ${platform.version}`,
|
||||
platform.os.toString(),
|
||||
).catch(() => {});
|
||||
};
|
||||
|
||||
const id = window.location.href.match('id=[0-9]+')[0].slice(3);
|
||||
callAnnotationUI(id);
|
||||
};
|
||||
@ -1,213 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* exported BorderSticker */
|
||||
|
||||
class BorderSticker {
|
||||
constructor(currentShape, frameContent, shapes, scale) {
|
||||
this._currentShape = currentShape;
|
||||
this._frameContent = frameContent;
|
||||
this._enabled = false;
|
||||
this._groups = null;
|
||||
this._scale = scale;
|
||||
this._accounter = {
|
||||
clicks: [],
|
||||
shapeID: null,
|
||||
};
|
||||
|
||||
const transformedShapes = shapes
|
||||
.filter((shape) => !shape.model.removed)
|
||||
.map((shape) => {
|
||||
const pos = shape.interpolation.position;
|
||||
// convert boxes to point sets
|
||||
if (!('points' in pos)) {
|
||||
return {
|
||||
points: window.cvat.translate.points
|
||||
.actualToCanvas(`${pos.xtl},${pos.ytl} ${pos.xbr},${pos.ytl}`
|
||||
+ ` ${pos.xbr},${pos.ybr} ${pos.xtl},${pos.ybr}`),
|
||||
color: shape.model.color.shape,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
points: window.cvat.translate.points
|
||||
.actualToCanvas(pos.points),
|
||||
color: shape.model.color.shape,
|
||||
};
|
||||
});
|
||||
|
||||
this._drawBorderMarkers(transformedShapes);
|
||||
}
|
||||
|
||||
_addRawPoint(x, y) {
|
||||
this._currentShape.array().valueOf().pop();
|
||||
this._currentShape.array().valueOf().push([x, y]);
|
||||
// not error, specific of the library
|
||||
this._currentShape.array().valueOf().push([x, y]);
|
||||
const paintHandler = this._currentShape.remember('_paintHandler');
|
||||
paintHandler.drawCircles();
|
||||
paintHandler.set.members.forEach((el) => {
|
||||
el.attr('stroke-width', 1 / this._scale).attr('r', 2.5 / this._scale);
|
||||
});
|
||||
this._currentShape.plot(this._currentShape.array().valueOf());
|
||||
}
|
||||
|
||||
_drawBorderMarkers(shapes) {
|
||||
const namespace = 'http://www.w3.org/2000/svg';
|
||||
|
||||
this._groups = shapes.reduce((acc, shape, shapeID) => {
|
||||
// Group all points by inside svg groups
|
||||
const group = window.document.createElementNS(namespace, 'g');
|
||||
shape.points.split(/\s/).map((point, pointID, points) => {
|
||||
const [x, y] = point.split(',').map((coordinate) => +coordinate);
|
||||
const circle = window.document.createElementNS(namespace, 'circle');
|
||||
circle.classList.add('shape-creator-border-point');
|
||||
circle.setAttribute('fill', shape.color);
|
||||
circle.setAttribute('stroke', 'black');
|
||||
circle.setAttribute('stroke-width', 1 / this._scale);
|
||||
circle.setAttribute('cx', +x);
|
||||
circle.setAttribute('cy', +y);
|
||||
circle.setAttribute('r', 5 / this._scale);
|
||||
|
||||
circle.doubleClickListener = (e) => {
|
||||
// Just for convenience (prevent screen fit feature)
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
circle.clickListener = (e) => {
|
||||
e.stopPropagation();
|
||||
// another shape was clicked
|
||||
if (this._accounter.shapeID !== null && this._accounter.shapeID !== shapeID) {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
this._accounter.shapeID = shapeID;
|
||||
|
||||
if (this._accounter.clicks[1] === pointID) {
|
||||
// the same point repeated two times
|
||||
const [_x, _y] = point.split(',').map((coordinate) => +coordinate);
|
||||
this._addRawPoint(_x, _y);
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
// the first point can not be clicked twice
|
||||
if (this._accounter.clicks[0] !== pointID) {
|
||||
this._accounter.clicks.push(pointID);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// up clicked group for convenience
|
||||
this._frameContent.node.appendChild(group);
|
||||
|
||||
// the first click
|
||||
if (this._accounter.clicks.length === 1) {
|
||||
// draw and remove initial point just to initialize data structures
|
||||
if (!this._currentShape.remember('_paintHandler').startPoint) {
|
||||
this._currentShape.draw('point', e);
|
||||
this._currentShape.draw('undo');
|
||||
}
|
||||
|
||||
const [_x, _y] = point.split(',').map((coordinate) => +coordinate);
|
||||
this._addRawPoint(_x, _y);
|
||||
// the second click
|
||||
} else if (this._accounter.clicks.length === 2) {
|
||||
circle.classList.add('shape-creator-border-point-direction');
|
||||
// the third click
|
||||
} else {
|
||||
// sign defines bypass direction
|
||||
const landmarks = this._accounter.clicks;
|
||||
const sign = Math.sign(landmarks[2] - landmarks[0])
|
||||
* Math.sign(landmarks[1] - landmarks[0])
|
||||
* Math.sign(landmarks[2] - landmarks[1]);
|
||||
|
||||
// go via a polygon and get vertexes
|
||||
// the first vertex has been already drawn
|
||||
const way = [];
|
||||
for (let i = landmarks[0] + sign; ; i += sign) {
|
||||
if (i < 0) {
|
||||
i = points.length - 1;
|
||||
} else if (i === points.length) {
|
||||
i = 0;
|
||||
}
|
||||
|
||||
way.push(points[i]);
|
||||
|
||||
if (i === this._accounter.clicks[this._accounter.clicks.length - 1]) {
|
||||
// put the last element twice
|
||||
// specific of svg.draw.js
|
||||
// way.push(points[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// remove the latest cursor position from drawing array
|
||||
for (const wayPoint of way) {
|
||||
const [_x, _y] = wayPoint.split(',').map((coordinate) => +coordinate);
|
||||
this._addRawPoint(_x, _y);
|
||||
}
|
||||
|
||||
this.reset();
|
||||
}
|
||||
};
|
||||
|
||||
circle.addEventListener('click', circle.clickListener);
|
||||
circle.addEventListener('dblclick', circle.doubleClickListener);
|
||||
|
||||
return circle;
|
||||
}).forEach((circle) => group.appendChild(circle));
|
||||
|
||||
acc.push(group);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
this._groups
|
||||
.forEach((group) => this._frameContent.node.appendChild(group));
|
||||
}
|
||||
|
||||
reset() {
|
||||
if (this._accounter.shapeID !== null) {
|
||||
while (this._accounter.clicks.length > 0) {
|
||||
const resetID = this._accounter.clicks.pop();
|
||||
this._groups[this._accounter.shapeID]
|
||||
.children[resetID].classList.remove('shape-creator-border-point-direction');
|
||||
}
|
||||
}
|
||||
|
||||
this._accounter = {
|
||||
clicks: [],
|
||||
shapeID: null,
|
||||
};
|
||||
}
|
||||
|
||||
disable() {
|
||||
if (this._groups) {
|
||||
this._groups.forEach((group) => {
|
||||
Array.from(group.children).forEach((circle) => {
|
||||
circle.removeEventListener('click', circle.clickListener);
|
||||
circle.removeEventListener('dblclick', circle.doubleClickListener);
|
||||
});
|
||||
|
||||
group.remove();
|
||||
});
|
||||
|
||||
this._groups = null;
|
||||
}
|
||||
}
|
||||
|
||||
scale(scale) {
|
||||
this._scale = scale;
|
||||
if (this._groups) {
|
||||
this._groups.forEach((group) => {
|
||||
Array.from(group.children).forEach((circle) => {
|
||||
circle.setAttribute('r', 5 / scale);
|
||||
circle.setAttribute('stroke-width', 1 / scale);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
platform:false
|
||||
*/
|
||||
|
||||
// legacy syntax for IE support
|
||||
|
||||
var supportedPlatforms = ['Chrome'];
|
||||
if (supportedPlatforms.indexOf(platform.name) === -1) {
|
||||
try {
|
||||
document.documentElement.innerHTML = "<center><h1> Your browser is detected as " + platform.name +
|
||||
". This tool does not support it. Please use the latest version of Google Chrome.</h1></center>";
|
||||
window.stop();
|
||||
} catch (err) {
|
||||
document.execCommand('Stop');
|
||||
}
|
||||
}
|
||||
@ -1,177 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* exported CoordinateTranslator */
|
||||
"use strict";
|
||||
|
||||
class CoordinateTranslator {
|
||||
constructor() {
|
||||
this._boxTranslator = {
|
||||
_playerOffset: 0,
|
||||
_convert(box, sign) {
|
||||
for (const prop of ['xtl', 'ytl', 'xbr', 'ybr', 'x', 'y']) {
|
||||
if (prop in box) {
|
||||
box[prop] += this._playerOffset * sign;
|
||||
}
|
||||
}
|
||||
|
||||
return box;
|
||||
},
|
||||
actualToCanvas(actualBox) {
|
||||
const canvasBox = {};
|
||||
for (const key in actualBox) {
|
||||
canvasBox[key] = actualBox[key];
|
||||
}
|
||||
|
||||
return this._convert(canvasBox, 1);
|
||||
},
|
||||
|
||||
canvasToActual(canvasBox) {
|
||||
const actualBox = {};
|
||||
for (const key in canvasBox) {
|
||||
actualBox[key] = canvasBox[key];
|
||||
}
|
||||
|
||||
return this._convert(actualBox, -1);
|
||||
},
|
||||
|
||||
canvasToClient(sourceCanvas, canvasBox) {
|
||||
const points = [
|
||||
[canvasBox.x, canvasBox.y],
|
||||
[canvasBox.x + canvasBox.width, canvasBox.y],
|
||||
[canvasBox.x, canvasBox.y + canvasBox.height],
|
||||
[canvasBox.x + canvasBox.width, canvasBox.y + canvasBox.height],
|
||||
].map(el => window.cvat.translate.point.canvasToClient(sourceCanvas, ...el));
|
||||
|
||||
const xes = points.map(el => el.x);
|
||||
const yes = points.map(el => el.y);
|
||||
|
||||
const xmin = Math.min(...xes);
|
||||
const xmax = Math.max(...xes);
|
||||
const ymin = Math.min(...yes);
|
||||
const ymax = Math.max(...yes);
|
||||
|
||||
return {
|
||||
x: xmin,
|
||||
y: ymin,
|
||||
width: xmax - xmin,
|
||||
height: ymax - ymin,
|
||||
};
|
||||
},
|
||||
|
||||
serverToClient(shape) {
|
||||
return {
|
||||
xtl: shape.points[0],
|
||||
ytl: shape.points[1],
|
||||
xbr: shape.points[2],
|
||||
ybr: shape.points[3],
|
||||
};
|
||||
},
|
||||
|
||||
clientToServer(clientObject) {
|
||||
return {
|
||||
points: [clientObject.xtl, clientObject.ytl,
|
||||
clientObject.xbr, clientObject.ybr],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
this._pointsTranslator = {
|
||||
_playerOffset: 0,
|
||||
_convert(points, sign) {
|
||||
if (typeof (points) === 'string') {
|
||||
return points.split(' ').map(coord => coord.split(',')
|
||||
.map(x => +x + this._playerOffset * sign).join(',')).join(' ');
|
||||
}
|
||||
if (typeof (points) === 'object') {
|
||||
return points.map(point => ({
|
||||
x: point.x + this._playerOffset * sign,
|
||||
y: point.y + this._playerOffset * sign,
|
||||
}));
|
||||
}
|
||||
throw Error('Unknown points type was found');
|
||||
},
|
||||
actualToCanvas(actualPoints) {
|
||||
return this._convert(actualPoints, 1);
|
||||
},
|
||||
|
||||
canvasToActual(canvasPoints) {
|
||||
return this._convert(canvasPoints, -1);
|
||||
},
|
||||
|
||||
serverToClient(shape) {
|
||||
return {
|
||||
points: shape.points.reduce((acc, el, idx) => {
|
||||
if (idx % 2) {
|
||||
acc.slice(-1)[0].push(el);
|
||||
} else {
|
||||
acc.push([el]);
|
||||
}
|
||||
return acc;
|
||||
}, []).map(point => point.join(',')).join(' '),
|
||||
};
|
||||
},
|
||||
|
||||
clientToServer(clientPoints) {
|
||||
return {
|
||||
points: clientPoints.points.split(' ').join(',').split(',').map(x => +x),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
this._pointTranslator = {
|
||||
_rotation: 0,
|
||||
clientToCanvas(targetCanvas, clientX, clientY) {
|
||||
let pt = targetCanvas.createSVGPoint();
|
||||
pt.x = clientX;
|
||||
pt.y = clientY;
|
||||
pt = pt.matrixTransform(targetCanvas.getScreenCTM().inverse());
|
||||
return pt;
|
||||
},
|
||||
canvasToClient(sourceCanvas, canvasX, canvasY) {
|
||||
let pt = sourceCanvas.createSVGPoint();
|
||||
pt.x = canvasX;
|
||||
pt.y = canvasY;
|
||||
pt = pt.matrixTransform(sourceCanvas.getScreenCTM());
|
||||
return pt;
|
||||
},
|
||||
rotate(x, y, cx, cy) {
|
||||
cx = (typeof cx === 'undefined' ? 0 : cx);
|
||||
cy = (typeof cy === 'undefined' ? 0 : cy);
|
||||
|
||||
const radians = (Math.PI / 180) * window.cvat.player.rotation;
|
||||
const cos = Math.cos(radians);
|
||||
const sin = Math.sin(radians);
|
||||
|
||||
return {
|
||||
x: (cos * (x - cx)) + (sin * (y - cy)) + cx,
|
||||
y: (cos * (y - cy)) - (sin * (x - cx)) + cy,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
get box() {
|
||||
return this._boxTranslator;
|
||||
}
|
||||
|
||||
get points() {
|
||||
return this._pointsTranslator;
|
||||
}
|
||||
|
||||
get point() {
|
||||
return this._pointTranslator;
|
||||
}
|
||||
|
||||
set playerOffset(value) {
|
||||
this._boxTranslator._playerOffset = value;
|
||||
this._pointsTranslator._playerOffset = value;
|
||||
}
|
||||
|
||||
set rotation(value) {
|
||||
this._pointTranslator._rotation = value;
|
||||
}
|
||||
}
|
||||
@ -1,219 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* exported HistoryModel HistoryController HistoryView */
|
||||
|
||||
/* global
|
||||
Listener:false
|
||||
Logger:false
|
||||
Mousetrap:false
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
class HistoryModel extends Listener {
|
||||
constructor(playerModel) {
|
||||
super('onHistoryUpdate', () => this );
|
||||
|
||||
this._deep = 128;
|
||||
this._id = 0;
|
||||
this._undo_stack = [];
|
||||
this._redo_stack = [];
|
||||
this._locked = false;
|
||||
this._player = playerModel;
|
||||
|
||||
window.cvat.addAction = (name, undo, redo, frame) => this.addAction(name, undo, redo, frame);
|
||||
}
|
||||
|
||||
undo() {
|
||||
let frame = window.cvat.player.frames.current;
|
||||
let undo = this._undo_stack.pop();
|
||||
|
||||
if (undo) {
|
||||
try {
|
||||
Logger.addEvent(Logger.EventType.undoAction, {
|
||||
name: undo.name,
|
||||
frame: undo.frame,
|
||||
});
|
||||
|
||||
if (undo.frame != frame) {
|
||||
this._player.shift(undo.frame, true);
|
||||
}
|
||||
this._locked = true;
|
||||
undo.undo();
|
||||
}
|
||||
catch(err) {
|
||||
this.notify();
|
||||
throw err;
|
||||
}
|
||||
finally {
|
||||
this._locked = false;
|
||||
}
|
||||
|
||||
this._redo_stack.push(undo);
|
||||
}
|
||||
|
||||
this.notify();
|
||||
}
|
||||
|
||||
redo() {
|
||||
let frame = window.cvat.player.frames.current;
|
||||
let redo = this._redo_stack.pop();
|
||||
|
||||
if (redo) {
|
||||
try {
|
||||
Logger.addEvent(Logger.EventType.redoAction, {
|
||||
name: redo.name,
|
||||
frame: redo.frame,
|
||||
});
|
||||
|
||||
if (redo.frame != frame) {
|
||||
this._player.shift(redo.frame, true);
|
||||
}
|
||||
this._locked = true;
|
||||
redo.redo();
|
||||
}
|
||||
catch(err) {
|
||||
this.notify();
|
||||
throw err;
|
||||
}
|
||||
finally {
|
||||
this._locked = false;
|
||||
}
|
||||
|
||||
this._undo_stack.push(redo);
|
||||
}
|
||||
|
||||
this.notify();
|
||||
}
|
||||
|
||||
addAction(name, undo, redo, frame) {
|
||||
if (this._locked) return;
|
||||
if (this._undo_stack.length >= this._deep) {
|
||||
this._undo_stack.shift();
|
||||
}
|
||||
|
||||
this._undo_stack.push({
|
||||
name: name,
|
||||
undo: undo,
|
||||
redo: redo,
|
||||
frame: frame,
|
||||
id: this._id++,
|
||||
});
|
||||
this._redo_stack = [];
|
||||
this.notify();
|
||||
}
|
||||
|
||||
empty() {
|
||||
this._undo_stack = [];
|
||||
this._redo_stack = [];
|
||||
this._id = 0;
|
||||
this.notify();
|
||||
}
|
||||
|
||||
get undoLength() {
|
||||
return this._undo_stack.length;
|
||||
}
|
||||
|
||||
get redoLength() {
|
||||
return this._redo_stack.length;
|
||||
}
|
||||
|
||||
get lastUndoText() {
|
||||
let lastUndo = this._undo_stack[this._undo_stack.length - 1];
|
||||
if (lastUndo) {
|
||||
return `${lastUndo.name} [Frame ${lastUndo.frame}] [Id ${lastUndo.id}]`;
|
||||
}
|
||||
else return 'None';
|
||||
}
|
||||
|
||||
get lastRedoText() {
|
||||
let lastRedo = this._redo_stack[this._redo_stack.length - 1];
|
||||
if (lastRedo) {
|
||||
return `${lastRedo.name} [Frame ${lastRedo.frame}] [Id ${lastRedo.id}]`;
|
||||
}
|
||||
else return 'None';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class HistoryController {
|
||||
constructor(model) {
|
||||
this._model = model;
|
||||
setupCollectionShortcuts.call(this);
|
||||
|
||||
function setupCollectionShortcuts() {
|
||||
let undoHandler = Logger.shortkeyLogDecorator(function(e) {
|
||||
this.undo();
|
||||
e.preventDefault();
|
||||
}.bind(this));
|
||||
|
||||
let redoHandler = Logger.shortkeyLogDecorator(function(e) {
|
||||
this.redo();
|
||||
e.preventDefault();
|
||||
}.bind(this));
|
||||
|
||||
let shortkeys = window.cvat.config.shortkeys;
|
||||
Mousetrap.bind(shortkeys["undo"].value, undoHandler.bind(this), 'keydown');
|
||||
Mousetrap.bind(shortkeys["redo"].value, redoHandler.bind(this), 'keydown');
|
||||
}
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (!window.cvat.mode) {
|
||||
this._model.undo();
|
||||
}
|
||||
}
|
||||
|
||||
redo() {
|
||||
if (!window.cvat.mode) {
|
||||
this._model.redo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class HistoryView {
|
||||
constructor(controller, model) {
|
||||
this._controller = controller;
|
||||
this._undoButton = $('#undoButton');
|
||||
this._redoButton = $('#redoButton');
|
||||
this._lastUndoText = $('#lastUndoText');
|
||||
this._lastRedoText = $('#lastRedoText');
|
||||
|
||||
let shortkeys = window.cvat.config.shortkeys;
|
||||
this._undoButton.attr('title', `${shortkeys['undo'].view_value} - ${shortkeys['undo'].description}`);
|
||||
this._redoButton.attr('title', `${shortkeys['redo'].view_value} - ${shortkeys['redo'].description}`);
|
||||
|
||||
this._undoButton.on('click', () => {
|
||||
this._controller.undo();
|
||||
});
|
||||
|
||||
this._redoButton.on('click', () => {
|
||||
this._controller.redo();
|
||||
});
|
||||
|
||||
model.subscribe(this);
|
||||
}
|
||||
|
||||
onHistoryUpdate(model) {
|
||||
if (model.undoLength) {
|
||||
this._undoButton.prop('disabled', false);
|
||||
}
|
||||
else {
|
||||
this._undoButton.prop('disabled', true);
|
||||
}
|
||||
|
||||
if (model.redoLength) {
|
||||
this._redoButton.prop('disabled', false);
|
||||
}
|
||||
else {
|
||||
this._redoButton.prop('disabled', true);
|
||||
}
|
||||
|
||||
this._lastUndoText.text(model.lastUndoText);
|
||||
this._lastRedoText.text(model.lastRedoText);
|
||||
}
|
||||
}
|
||||
@ -1,192 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* exported LabelsInfo */
|
||||
|
||||
class LabelsInfo {
|
||||
constructor(labels) {
|
||||
function convertAttribute(attribute) {
|
||||
return {
|
||||
mutable: attribute.mutable,
|
||||
type: attribute.input_type,
|
||||
name: attribute.name,
|
||||
values: attribute.input_type === 'checkbox'
|
||||
? [attribute.values[0].toLowerCase() !== 'false'] : attribute.values,
|
||||
};
|
||||
}
|
||||
|
||||
this._labels = {};
|
||||
this._attributes = {};
|
||||
this._colorIdxs = {};
|
||||
|
||||
for (const label of labels) {
|
||||
this._labels[label.id] = {
|
||||
name: label.name,
|
||||
attributes: {},
|
||||
};
|
||||
|
||||
for (const attr of label.attributes) {
|
||||
this._attributes[attr.id] = convertAttribute(attr);
|
||||
this._labels[label.id].attributes[attr.id] = this._attributes[attr.id];
|
||||
}
|
||||
|
||||
this._colorIdxs[label.id] = +label.id;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
labelColorIdx(labelId) {
|
||||
return this._colorIdxs[labelId];
|
||||
}
|
||||
|
||||
|
||||
updateLabelColorIdx(labelId) {
|
||||
if (labelId in this._colorIdxs) {
|
||||
this._colorIdxs[labelId] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
labels() {
|
||||
const labels = {};
|
||||
for (const labelId in this._labels) {
|
||||
if (Object.prototype.hasOwnProperty.call(this._labels, labelId)) {
|
||||
labels[labelId] = this._labels[labelId].name;
|
||||
}
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
|
||||
labelAttributes(labelId) {
|
||||
if (labelId in this._labels) {
|
||||
const attributes = {};
|
||||
const labelAttributes = this._labels[labelId].attributes;
|
||||
for (const attrId in labelAttributes) {
|
||||
if (Object.prototype.hasOwnProperty.call(labelAttributes, attrId)) {
|
||||
attributes[attrId] = labelAttributes[attrId].name;
|
||||
}
|
||||
}
|
||||
return attributes;
|
||||
}
|
||||
throw Error('Unknown label ID');
|
||||
}
|
||||
|
||||
|
||||
attributes() {
|
||||
const attributes = {};
|
||||
for (const attrId in this._attributes) {
|
||||
if (Object.prototype.hasOwnProperty.call(this._attributes, attrId)) {
|
||||
attributes[attrId] = this._attributes[attrId].name;
|
||||
}
|
||||
}
|
||||
return attributes;
|
||||
}
|
||||
|
||||
|
||||
attrInfo(attrId) {
|
||||
if (attrId in this._attributes) {
|
||||
return JSON.parse(JSON.stringify(this._attributes[attrId]));
|
||||
}
|
||||
throw Error('Unknown attribute ID');
|
||||
}
|
||||
|
||||
|
||||
labelIdOf(name) {
|
||||
for (const labelId in this._labels) {
|
||||
if (this._labels[labelId].name === name) {
|
||||
return +labelId;
|
||||
}
|
||||
}
|
||||
throw Error('Unknown label name');
|
||||
}
|
||||
|
||||
|
||||
attrIdOf(labelId, name) {
|
||||
const attributes = this.labelAttributes(labelId);
|
||||
for (const attrId in attributes) {
|
||||
if (this._attributes[attrId].name === name) {
|
||||
return +attrId;
|
||||
}
|
||||
}
|
||||
throw Error('Unknown attribute name');
|
||||
}
|
||||
|
||||
|
||||
static normalize(type, attrValue) {
|
||||
const value = String(attrValue);
|
||||
if (type === 'checkbox') {
|
||||
return value !== '0' && value.toLowerCase() !== 'false';
|
||||
}
|
||||
|
||||
if (type === 'text') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (type === 'number') {
|
||||
if (Number.isNaN(+value)) {
|
||||
throw Error(`Can not convert ${value} to number.`);
|
||||
} else {
|
||||
return +value;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
static serialize(deserialized) {
|
||||
let serialized = '';
|
||||
for (const label of deserialized) {
|
||||
serialized += ` ${label.name}`;
|
||||
for (const attr of label.attributes) {
|
||||
serialized += ` ${attr.mutable ? '~' : '@'}`;
|
||||
serialized += `${attr.input_type}=${attr.name}:`;
|
||||
serialized += attr.values.join(',');
|
||||
}
|
||||
}
|
||||
|
||||
return serialized.trim();
|
||||
}
|
||||
|
||||
|
||||
static deserialize(serialized) {
|
||||
const normalized = serialized.replace(/'+/g, '\'').replace(/\s+/g, ' ').trim();
|
||||
const fragments = String.customSplit(normalized, ' ');
|
||||
|
||||
const deserialized = [];
|
||||
let latest = null;
|
||||
for (let fragment of fragments) {
|
||||
fragment = fragment.trim();
|
||||
if ((fragment.startsWith('~')) || (fragment.startsWith('@'))) {
|
||||
const regex = /(@|~)(checkbox|select|number|text|radio)=(.+):(.+)/g;
|
||||
const result = regex.exec(fragment);
|
||||
if (result === null || latest === null) {
|
||||
throw Error('Bad labels format');
|
||||
}
|
||||
|
||||
const values = String.customSplit(result[4], ',');
|
||||
latest.attributes.push({
|
||||
name: result[3].replace(/^"/, '').replace(/"$/, ''),
|
||||
mutable: result[1] === '~',
|
||||
input_type: result[2],
|
||||
default_value: values[0].replace(/^"/, '').replace(/"$/, ''),
|
||||
values: values.map(val => val.replace(/^"/, '').replace(/"$/, '')),
|
||||
});
|
||||
} else {
|
||||
latest = {
|
||||
name: fragment.replace(/^"/, '').replace(/"$/, ''),
|
||||
attributes: [],
|
||||
};
|
||||
|
||||
deserialized.push(latest);
|
||||
}
|
||||
}
|
||||
return deserialized;
|
||||
}
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* exported Listener */
|
||||
"use strict";
|
||||
|
||||
class Listener {
|
||||
constructor(notifyCallbackName, getStateCallback) {
|
||||
this._listeners = [];
|
||||
this._notifyCallbackName = notifyCallbackName;
|
||||
this._getStateCallback = getStateCallback;
|
||||
}
|
||||
|
||||
subscribe(listener) {
|
||||
if (typeof(listener) != 'object') {
|
||||
throw Error('Bad listener for subscribe found. Listener is not object.');
|
||||
}
|
||||
|
||||
if (typeof(listener[this._notifyCallbackName]) != 'function') {
|
||||
throw Error('Bad listener for subscribe found. Listener does not have a callback function ' + this._notifyCallbackName);
|
||||
}
|
||||
|
||||
if (this._listeners.indexOf(listener) === -1) {
|
||||
this._listeners.push(listener);
|
||||
}
|
||||
}
|
||||
|
||||
unsubscribeAll() {
|
||||
this._listeners = [];
|
||||
}
|
||||
|
||||
unsubscribe(listener) {
|
||||
let idx = this._listeners.indexOf(listener);
|
||||
if (idx != -1) {
|
||||
this._listeners.splice(idx,1);
|
||||
}
|
||||
else {
|
||||
throw Error('Unknown listener for unsubscribe');
|
||||
}
|
||||
}
|
||||
|
||||
notify() {
|
||||
let state = this._getStateCallback();
|
||||
for (let listener of this._listeners) {
|
||||
listener[this._notifyCallbackName](state);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,564 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* exported Logger */
|
||||
|
||||
/* global
|
||||
Cookies:false
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
class UserActivityHandler {
|
||||
constructor() {
|
||||
this._TIME_TRESHHOLD = 100000; //ms
|
||||
this._prevEventTime = Date.now();
|
||||
this._workingTime = 0;
|
||||
}
|
||||
|
||||
updateTimer() {
|
||||
if (document.hasFocus()) {
|
||||
let now = Date.now();
|
||||
let diff = now - this._prevEventTime;
|
||||
this._prevEventTime = now;
|
||||
this._workingTime += diff < this._TIME_TRESHHOLD ? diff : 0;
|
||||
}
|
||||
}
|
||||
|
||||
resetTimer() {
|
||||
this._prevEventTime = Date.now();
|
||||
this._workingTime = 0;
|
||||
}
|
||||
|
||||
getWorkingTime() {
|
||||
return this._workingTime;
|
||||
}
|
||||
}
|
||||
|
||||
class LogCollection extends Array {
|
||||
constructor(logger, items) {
|
||||
super(items.length);
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
super[i] = items[i];
|
||||
}
|
||||
this._loggerHandler = logger;
|
||||
}
|
||||
|
||||
save() {
|
||||
this._loggerHandler.pushLogs(this);
|
||||
}
|
||||
|
||||
export() {
|
||||
return Array.from(this, log => log.serialize());
|
||||
}
|
||||
}
|
||||
|
||||
class LoggerHandler {
|
||||
constructor(jobId) {
|
||||
this._clientID = Date.now().toString().substr(-6);
|
||||
this._jobId = jobId;
|
||||
this._logEvents = [];
|
||||
this._userActivityHandler = new UserActivityHandler();
|
||||
this._timeThresholds = {};
|
||||
}
|
||||
|
||||
addEvent(event) {
|
||||
this._pushEvent(event);
|
||||
}
|
||||
|
||||
addContinuedEvent(event) {
|
||||
this._userActivityHandler.updateTimer();
|
||||
event.onCloseCallback = this._closeCallback.bind(this);
|
||||
return event;
|
||||
}
|
||||
|
||||
sendExceptions(exception) {
|
||||
this._extendEvent(exception);
|
||||
return new Promise((resolve, reject) => {
|
||||
let retries = 3;
|
||||
let makeRequest = () => {
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/api/v1/server/exception');
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.setRequestHeader("X-CSRFToken", Cookies.get('csrftoken'));
|
||||
let onreject = () => {
|
||||
if (retries--) {
|
||||
setTimeout(() => makeRequest(), 30000); //30 sec delay
|
||||
} else {
|
||||
let payload = exception.serialize();
|
||||
delete Object.assign(payload, {origin_message: payload.message }).message;
|
||||
this.addEvent(new Logger.LogEvent(
|
||||
Logger.EventType.sendException,
|
||||
payload,
|
||||
"Can't send exception",
|
||||
));
|
||||
reject({
|
||||
status: xhr.status,
|
||||
statusText: xhr.statusText,
|
||||
});
|
||||
}
|
||||
};
|
||||
xhr.onload = () => {
|
||||
switch (xhr.status) {
|
||||
case 201:
|
||||
case 403: // ignore forbidden response
|
||||
resolve(xhr.response);
|
||||
break;
|
||||
|
||||
default:
|
||||
onreject();
|
||||
}
|
||||
};
|
||||
xhr.onerror = () => {
|
||||
// if status === 0 a request is a failure on the network level, ignore it
|
||||
if (xhr.status === 0) {
|
||||
resolve(xhr.response);
|
||||
} else {
|
||||
onreject();
|
||||
}
|
||||
};
|
||||
xhr.send(JSON.stringify(exception.serialize()));
|
||||
};
|
||||
makeRequest();
|
||||
});
|
||||
}
|
||||
|
||||
getLogs() {
|
||||
let logs = new LogCollection(this, this._logEvents);
|
||||
this._logEvents.length = 0;
|
||||
return logs;
|
||||
}
|
||||
|
||||
pushLogs(logEvents) {
|
||||
Array.prototype.push.apply(this._logEvents, logEvents);
|
||||
}
|
||||
|
||||
_extendEvent(event) {
|
||||
event._jobId = this._jobId;
|
||||
event._clientId = this._clientID;
|
||||
}
|
||||
|
||||
_pushEvent(event) {
|
||||
this._extendEvent(event);
|
||||
if (event._type in this._timeThresholds) {
|
||||
this._timeThresholds[event._type].wait(event);
|
||||
}
|
||||
else {
|
||||
this._logEvents.push(event);
|
||||
}
|
||||
this._userActivityHandler.updateTimer();
|
||||
}
|
||||
|
||||
_closeCallback(event) {
|
||||
this._pushEvent(event);
|
||||
}
|
||||
|
||||
updateTimer() {
|
||||
this._userActivityHandler.updateTimer();
|
||||
}
|
||||
|
||||
resetTimer() {
|
||||
this._userActivityHandler.resetTimer();
|
||||
}
|
||||
|
||||
getWorkingTime() {
|
||||
return this._userActivityHandler.getWorkingTime();
|
||||
}
|
||||
|
||||
setTimeThreshold(eventType, threshold) {
|
||||
this._timeThresholds[eventType] = {
|
||||
_threshold: threshold,
|
||||
_timeoutHandler: null,
|
||||
_timestamp: 0,
|
||||
_event: null,
|
||||
_logEvents: this._logEvents,
|
||||
wait: function(event) {
|
||||
if (this._event) {
|
||||
if (this._timeoutHandler) {
|
||||
clearTimeout(this._timeoutHandler);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this._timestamp = event._timestamp;
|
||||
}
|
||||
this._event = event;
|
||||
this._timeoutHandler = setTimeout(() => {
|
||||
if ('duration' in this._event._values) {
|
||||
this._event._values.duration += this._event._timestamp - this._timestamp;
|
||||
}
|
||||
this._event._timestamp = this._timestamp;
|
||||
this._logEvents.push(this._event);
|
||||
this._event = null;
|
||||
}, threshold);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Log message has simple json format - each message is set of "key" : "value"
|
||||
pairs inside curly braces - {"key1" : "string_value", "key2" : number_value,
|
||||
...} Value may be string or number (see json spec) required fields for all event
|
||||
types:
|
||||
|
||||
NAME TYPE DESCRIPTION
|
||||
"event" string see EventType enum description of possible values.
|
||||
"timestamp" number timestamp in UNIX format - the number of seconds
|
||||
or milliseconds that have elapsed since 00:00:00
|
||||
Thursday, 1 January 1970
|
||||
"application" string application name
|
||||
"userid" string Unique userid
|
||||
"task" string Unique task id. (Is expected corresponding Jira task id)
|
||||
|
||||
"count" is requiered field for "Add object", "Delete object", "Copy track",
|
||||
"Propagate object", "Merge objecrs", "Undo action" and "Redo action" events with
|
||||
number value.
|
||||
|
||||
Example : { "event" : "Add object", "timestamp" : 1486040342867, "application" :
|
||||
"CVAT", "duration" : 4200, "userid" : "ESAZON1X-MOBL", "count" : 1, "type" :
|
||||
"bounding box" }
|
||||
|
||||
Types of supported events. Minimum subset of events to generate simple report
|
||||
are Logger.EventType.addObject, Logger.EventType.deleteObject and
|
||||
Logger.EventType.sendTaskInfo. Value of "count" property should be a number.
|
||||
*/
|
||||
|
||||
class LoggerEvent {
|
||||
constructor(type, message) {
|
||||
this._time = new Date().toISOString();
|
||||
this._clientId = null;
|
||||
this._jobId = null;
|
||||
this._type = type;
|
||||
this._message = message;
|
||||
}
|
||||
|
||||
serialize() {
|
||||
let serializedObj = {
|
||||
job_id: this._jobId,
|
||||
client_id: this._clientId,
|
||||
name: Logger.eventTypeToString(this._type),
|
||||
time: this._time,
|
||||
};
|
||||
if (this._message) {
|
||||
Object.assign(serializedObj, { message: this._message,});
|
||||
}
|
||||
return serializedObj;
|
||||
}
|
||||
}
|
||||
|
||||
var Logger = {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_logger: null,
|
||||
_userActivityHandler: null,
|
||||
|
||||
/**
|
||||
* Logger.LogEvent class declaration
|
||||
* @param {Logger.EventType} type Type of event
|
||||
* @param {Object} values Any event values, for example {count: 1, label: 'vehicle'}
|
||||
* @param {Function} closeCallback callback function which will be called by close method. Setted by
|
||||
*/
|
||||
LogEvent: class extends LoggerEvent {
|
||||
constructor(type, values, message) {
|
||||
super(type, message);
|
||||
|
||||
this._timestamp = Date.now();
|
||||
this.onCloseCallback = null;
|
||||
this._is_active = document.hasFocus();
|
||||
this._values = values || {};
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return Object.assign(super.serialize(), {
|
||||
payload: this._values,
|
||||
is_active: this._is_active,
|
||||
});
|
||||
};
|
||||
|
||||
close(endTimestamp) {
|
||||
if (this.onCloseCallback) {
|
||||
this.addValues({
|
||||
duration: endTimestamp ? endTimestamp - this._timestamp : Date.now() - this._timestamp,
|
||||
});
|
||||
this.onCloseCallback(this);
|
||||
}
|
||||
};
|
||||
|
||||
addValues(values) {
|
||||
Object.assign(this._values, values);
|
||||
};
|
||||
},
|
||||
|
||||
ExceptionEvent: class extends LoggerEvent {
|
||||
constructor(message, filename, line, column, stack, client, system) {
|
||||
super(Logger.EventType.sendException, message);
|
||||
|
||||
this._client = client;
|
||||
this._column = column;
|
||||
this._filename = filename;
|
||||
this._line = line;
|
||||
this._stack = stack;
|
||||
this._system = system;
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return Object.assign(super.serialize(), {
|
||||
client: this._client,
|
||||
column: this._column,
|
||||
filename: this._filename,
|
||||
line: this._line,
|
||||
stack: this._stack,
|
||||
system: this._system,
|
||||
});
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Logger.EventType Enumeration.
|
||||
*/
|
||||
EventType: {
|
||||
// dumped as "Paste object". There are no additional required fields.
|
||||
pasteObject: 0,
|
||||
// dumped as "Change attribute". There are no additional required
|
||||
// fields.
|
||||
changeAttribute: 1,
|
||||
// dumped as "Drag object". There are no additional required fields.
|
||||
dragObject: 2,
|
||||
// dumped as "Delete object". "count" is required field, value of
|
||||
// deleted objects should be positive number.
|
||||
deleteObject: 3,
|
||||
// dumped as "Press shortcut". There are no additional required fields.
|
||||
pressShortcut: 4,
|
||||
// dumped as "Resize object". There are no additional required fields.
|
||||
resizeObject: 5,
|
||||
// dumped as "Send logs". It's expected that event has "duration" field,
|
||||
// but it isn't necessary.
|
||||
sendLogs: 6,
|
||||
// dumped as "Save job". It's expected that event has "duration" field,
|
||||
// but it isn't necessary.
|
||||
saveJob: 7,
|
||||
// dumped as "Jump frame". There are no additional required fields.
|
||||
jumpFrame: 8,
|
||||
// dumped as "Draw object". It's expected that event has "duration"
|
||||
// field, but it isn't necessary.
|
||||
drawObject: 9,
|
||||
// dumped as "Change label".
|
||||
changeLabel: 10,
|
||||
// dumped as "Send task info". "track count", "frame count", "object
|
||||
// count" are required fields. It's expected that event has
|
||||
// "current_frame" field.
|
||||
sendTaskInfo: 11,
|
||||
// dumped as "Load job". "track count", "frame count", "object count"
|
||||
// are required fields. It's expected that event has "duration" field,
|
||||
// but it isn't necessary.
|
||||
loadJob: 12,
|
||||
// dumped as "Move image". It's expected that event has "duration"
|
||||
// field, but it isn't necessary.
|
||||
moveImage: 13,
|
||||
// dumped as "Zoom image". It's expected that event has "duration"
|
||||
// field, but it isn't necessary.
|
||||
zoomImage: 14,
|
||||
// dumped as "Lock object". There are no additional required fields.
|
||||
lockObject: 15,
|
||||
// dumped as "Merge objects". "count" is required field with positive or
|
||||
// negative number value.
|
||||
mergeObjects: 16,
|
||||
// dumped as "Copy object". "count" is required field with number value.
|
||||
copyObject: 17,
|
||||
// dumped as "Propagate object". "count" is required field with number
|
||||
// value.
|
||||
propagateObject: 18,
|
||||
// dumped as "Undo action". "count" is required field with positive or
|
||||
// negative number value.
|
||||
undoAction: 19,
|
||||
// dumped as "Redo action". "count" is required field with positive or
|
||||
// negative number value.
|
||||
redoAction: 20,
|
||||
// dumped as "Send user activity". "working_time" is required field with
|
||||
// positive number value.
|
||||
sendUserActivity: 21,
|
||||
// dumped as "Send exception". Use to send any exception events to the
|
||||
// server. "message", "filename", "line" are mandatory fields. "stack"
|
||||
// and "column" are optional.
|
||||
sendException: 22,
|
||||
// dumped as "Change frame". There are no additional required fields.
|
||||
changeFrame: 23,
|
||||
// dumped as "Debug info". There are no additional required fields.
|
||||
debugInfo: 24,
|
||||
// dumped as "Fit image". There are no additional required fields.
|
||||
fitImage: 25,
|
||||
// dumped as "Rotate image". There are no additional required fields.
|
||||
rotateImage: 26,
|
||||
},
|
||||
|
||||
/**
|
||||
* Logger.initializeLogger
|
||||
* @param {String} applicationName application name
|
||||
* @param {String} taskId Task identificator (i.e. link to Jira)
|
||||
* @param {String} serverURL server url to recive logs
|
||||
* @return {Bool} true if initialization was succeed
|
||||
* @static
|
||||
*/
|
||||
initializeLogger: function(jobId) {
|
||||
if (!this._logger)
|
||||
{
|
||||
this._logger = new LoggerHandler(jobId);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Logger.addEvent Use this method to add a log event without duration field.
|
||||
* @param {Logger.EventType} type Event Type
|
||||
* @param {Object} values Any event values, for example {count: 1, label: 'vehicle'}
|
||||
* @param {String} message Any string message. Empty by default.
|
||||
* @static
|
||||
*/
|
||||
addEvent: function(type, values, message='') {
|
||||
this._logger.addEvent(new Logger.LogEvent(type, values, message));
|
||||
},
|
||||
|
||||
/**
|
||||
* Logger.addContinuedEvent Use to add log event with duration field.
|
||||
* Duration will be calculated automatically when LogEvent.close() method of
|
||||
* returned Object will be called. Note: in case of LogEvent.close() method
|
||||
* will not be callsed event will not be sent to server
|
||||
* @param {Logger.EventType} type Event Type
|
||||
* @param {Object} values Any event values, for example {count: 1, label:
|
||||
* 'vehicle'}
|
||||
* @param {String} message Any string message. Empty by default.
|
||||
* @return {LogEvent} instance of LogEvent
|
||||
* @static
|
||||
*/
|
||||
addContinuedEvent: function(type, values, message='') {
|
||||
return this._logger.addContinuedEvent(new Logger.LogEvent(type, values, message));
|
||||
},
|
||||
|
||||
/**
|
||||
* Logger.shortkeyLogDecorator use for decorating the shortkey handlers.
|
||||
* This decorator just create appropriate log event and close it when
|
||||
* decored function will performed.
|
||||
* @param {Function} decoredFunc is function for decorating
|
||||
* @return {Function} is decorated decoredFunc
|
||||
* @static
|
||||
*/
|
||||
shortkeyLogDecorator: function(decoredFunc) {
|
||||
let self = this;
|
||||
return function(e, combo) {
|
||||
if (window.cvat.frozen) {
|
||||
return;
|
||||
}
|
||||
let pressKeyEvent = self.addContinuedEvent(self.EventType.pressShortcut, {key: combo});
|
||||
let returnValue = decoredFunc(e, combo);
|
||||
pressKeyEvent.close();
|
||||
return returnValue;
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Logger.sendLogs Try to send exception logs to the server immediately.
|
||||
* @return {Promise}
|
||||
* @param {LogEvent} exceptionEvent
|
||||
* @static
|
||||
*/
|
||||
|
||||
sendException: function(message, filename, line, column, stack, client, system) {
|
||||
return this._logger.sendExceptions(
|
||||
new Logger.ExceptionEvent(
|
||||
message,
|
||||
filename,
|
||||
line,
|
||||
column,
|
||||
stack,
|
||||
client,
|
||||
system
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Logger.getLogs Remove and return collected Array of LogEvents from Logger
|
||||
* @return {Array}
|
||||
* @static
|
||||
*/
|
||||
getLogs: function(appendUserActivity=true)
|
||||
{
|
||||
if (appendUserActivity)
|
||||
{
|
||||
this.addEvent(Logger.EventType.sendUserActivity, {'working time': this._logger.getWorkingTime()});
|
||||
this._logger.resetTimer();
|
||||
}
|
||||
|
||||
return this._logger.getLogs();
|
||||
},
|
||||
|
||||
/** Logger.updateUserActivityTimer method updates internal timer for working
|
||||
* time calculation logic
|
||||
* @static
|
||||
*/
|
||||
updateUserActivityTimer: function()
|
||||
{
|
||||
this._logger.updateTimer();
|
||||
},
|
||||
|
||||
/** Logger.setTimeThreshold set time threshold in ms for EventType. If time
|
||||
* interval betwwen incoming log events less than threshold events will be
|
||||
* collapsed. Note that result event will have timestamp of first event, In
|
||||
* case of time threshold used for continued event duration will be
|
||||
* difference between first and last event timestamps and other fields from
|
||||
* last event.
|
||||
* @static
|
||||
* @param {Logger.EventType} eventType
|
||||
* @param {Number} threshold
|
||||
*/
|
||||
setTimeThreshold: function(eventType, threshold=500)
|
||||
{
|
||||
this._logger.setTimeThreshold(eventType, threshold);
|
||||
},
|
||||
|
||||
/** Logger._eventTypeToString private method to transform Logger.EventType
|
||||
* to string
|
||||
* @param {Logger.EventType} type Event Type
|
||||
* @return {String} string reppresentation of Logger.EventType
|
||||
* @static
|
||||
*/
|
||||
eventTypeToString: function(type)
|
||||
{
|
||||
switch(type) {
|
||||
case this.EventType.pasteObject: return 'Paste object';
|
||||
case this.EventType.changeAttribute: return 'Change attribute';
|
||||
case this.EventType.dragObject: return 'Drag object';
|
||||
case this.EventType.deleteObject: return 'Delete object';
|
||||
case this.EventType.pressShortcut: return 'Press shortcut';
|
||||
case this.EventType.resizeObject: return 'Resize object';
|
||||
case this.EventType.sendLogs: return 'Send logs';
|
||||
case this.EventType.saveJob: return 'Save job';
|
||||
case this.EventType.jumpFrame: return 'Jump frame';
|
||||
case this.EventType.drawObject: return 'Draw object';
|
||||
case this.EventType.changeLabel: return 'Change label';
|
||||
case this.EventType.sendTaskInfo: return 'Send task info';
|
||||
case this.EventType.loadJob: return 'Load job';
|
||||
case this.EventType.moveImage: return 'Move image';
|
||||
case this.EventType.zoomImage: return 'Zoom image';
|
||||
case this.EventType.lockObject: return 'Lock object';
|
||||
case this.EventType.mergeObjects: return 'Merge objects';
|
||||
case this.EventType.copyObject: return 'Copy object';
|
||||
case this.EventType.propagateObject: return 'Propagate object';
|
||||
case this.EventType.undoAction: return 'Undo action';
|
||||
case this.EventType.redoAction: return 'Redo action';
|
||||
case this.EventType.sendUserActivity: return 'Send user activity';
|
||||
case this.EventType.sendException: return 'Send exception';
|
||||
case this.EventType.changeFrame: return 'Change frame';
|
||||
case this.EventType.debugInfo: return 'Debug info';
|
||||
case this.EventType.fitImage: return 'Fit image';
|
||||
case this.EventType.rotateImage: return 'Rotate image';
|
||||
default: return 'Unknown';
|
||||
}
|
||||
},
|
||||
};
|
||||
@ -1,494 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* exported PolyshapeEditorModel PolyshapeEditorController PolyshapeEditorView */
|
||||
|
||||
/* global
|
||||
Listener:false
|
||||
POINT_RADIUS:false
|
||||
PolyShapeModel:false
|
||||
STROKE_WIDTH:false
|
||||
SVG:false
|
||||
BorderSticker:false
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
class PolyshapeEditorModel extends Listener {
|
||||
constructor(shapeCollection) {
|
||||
super("onPolyshapeEditorUpdate", () => this);
|
||||
|
||||
this._modeName = 'poly_editing';
|
||||
this._active = false;
|
||||
this._shapeCollection = shapeCollection;
|
||||
this._data = {
|
||||
points: null,
|
||||
color: null,
|
||||
start: null,
|
||||
oncomplete: null,
|
||||
type: null,
|
||||
event: null,
|
||||
startPoint: null,
|
||||
id: null,
|
||||
};
|
||||
}
|
||||
|
||||
edit(type, points, color, start, startPoint, e, oncomplete, id) {
|
||||
if (!this._active && !window.cvat.mode) {
|
||||
window.cvat.mode = this._modeName;
|
||||
this._active = true;
|
||||
this._data.points = points;
|
||||
this._data.color = color;
|
||||
this._data.start = start;
|
||||
this._data.oncomplete = oncomplete;
|
||||
this._data.type = type;
|
||||
this._data.event = e;
|
||||
this._data.startPoint = startPoint;
|
||||
this._data.id = id;
|
||||
this.notify();
|
||||
}
|
||||
else if (this._active) {
|
||||
throw Error('Polyshape has been being edited already');
|
||||
}
|
||||
}
|
||||
|
||||
finish(points) {
|
||||
if (this._active && this._data.oncomplete) {
|
||||
this._data.oncomplete(points);
|
||||
}
|
||||
|
||||
this.cancel();
|
||||
}
|
||||
|
||||
cancel() {
|
||||
if (this._active) {
|
||||
this._active = false;
|
||||
if (window.cvat.mode != this._modeName) {
|
||||
throw Error(`Inconsistent behaviour has been detected. Edit mode is activated, but mode variable is '${window.cvat.mode}'`);
|
||||
}
|
||||
else {
|
||||
window.cvat.mode = null;
|
||||
}
|
||||
|
||||
this._data.points = null;
|
||||
this._data.color = null;
|
||||
this._data.start = null;
|
||||
this._data.oncomplete = null;
|
||||
this._data.type = null;
|
||||
this._data.event = null;
|
||||
this._data.startPoint = null;
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
|
||||
get active() {
|
||||
return this._active;
|
||||
}
|
||||
|
||||
get data() {
|
||||
return this._data;
|
||||
}
|
||||
|
||||
get currentShapes() {
|
||||
this._shapeCollection.update();
|
||||
return this._shapeCollection.currentShapes;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class PolyshapeEditorController {
|
||||
constructor(model) {
|
||||
this._model = model;
|
||||
}
|
||||
|
||||
finish(points) {
|
||||
this._model.finish(points);
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this._model.cancel();
|
||||
}
|
||||
|
||||
get currentShapes() {
|
||||
return this._model.currentShapes;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class PolyshapeEditorView {
|
||||
constructor(model, controller) {
|
||||
this._controller = controller;
|
||||
this._data = null;
|
||||
|
||||
this._frameContent = SVG.adopt($('#frameContent')[0]);
|
||||
this._commonBordersCheckbox = $('#commonBordersCheckbox');
|
||||
this._originalShapePointsGroup = null;
|
||||
this._originalShapePoints = [];
|
||||
this._originalShape = null;
|
||||
this._correctLine = null;
|
||||
this._borderSticker = null;
|
||||
|
||||
this._scale = window.cvat.player.geometry.scale;
|
||||
this._frame = window.cvat.player.frames.current;
|
||||
|
||||
this._commonBordersCheckbox.on('change.shapeEditor', (e) => {
|
||||
if (this._correctLine) {
|
||||
if (!e.target.checked) {
|
||||
if (this._borderSticker) {
|
||||
this._borderSticker.disable();
|
||||
this._borderSticker = null;
|
||||
}
|
||||
} else {
|
||||
this._borderSticker = new BorderSticker(this._correctLine, this._frameContent,
|
||||
this._controller.currentShapes
|
||||
.filter((shape) => shape.model.id !== this._data.id),
|
||||
this._scale);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
model.subscribe(this);
|
||||
}
|
||||
|
||||
_rescaleDrawPoints() {
|
||||
let scale = this._scale;
|
||||
$('.svg_draw_point').each(function() {
|
||||
this.instance.radius(POINT_RADIUS / (2 * scale)).attr('stroke-width', STROKE_WIDTH / (2 * scale));
|
||||
});
|
||||
}
|
||||
|
||||
// After this method start element will be in begin of the array.
|
||||
// Array will consist only range elements from start to stop
|
||||
_resortPoints(points, start, stop) {
|
||||
let sorted = [];
|
||||
|
||||
if (points.indexOf(start) === -1 || points.indexOf(stop) === -1) {
|
||||
throw Error('Point array must consist both start and stop elements');
|
||||
}
|
||||
|
||||
let idx = points.indexOf(start) + 1;
|
||||
let condition = true; // constant condition is eslint error
|
||||
while (condition) {
|
||||
if (idx >= points.length) idx = 0;
|
||||
if (points[idx] === stop) condition = false;
|
||||
else sorted.push(points[idx++]);
|
||||
}
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
// Method represents array like circle list and find shortest way from source to target
|
||||
// It returns integer number - distance from source to target.
|
||||
// It can be negative if shortest way is anti clockwise
|
||||
_findMinCircleDistance(array, source, target) {
|
||||
let clockwise_distance = 0;
|
||||
let anti_clockwise_distance = 0;
|
||||
|
||||
let source_idx = array.indexOf(source);
|
||||
let target_idx = array.indexOf(target);
|
||||
|
||||
if (source_idx === -1 || target_idx == -1) {
|
||||
throw Error('Array should consist both elements');
|
||||
}
|
||||
|
||||
let idx = source_idx;
|
||||
while (array[idx++] != target) {
|
||||
clockwise_distance ++;
|
||||
if (idx >= array.length) idx = 0;
|
||||
}
|
||||
|
||||
idx = source_idx;
|
||||
while (array[idx--] != target) {
|
||||
anti_clockwise_distance ++;
|
||||
if (idx < 0) idx = array.length - 1;
|
||||
}
|
||||
|
||||
let offset = Math.min(clockwise_distance, anti_clockwise_distance);
|
||||
if (anti_clockwise_distance < clockwise_distance) {
|
||||
offset = -offset;
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
_addRawPoint(x, y) {
|
||||
this._correctLine.array().valueOf().pop();
|
||||
this._correctLine.array().valueOf().push([x, y]);
|
||||
// not error, specific of the library
|
||||
this._correctLine.array().valueOf().push([x, y]);
|
||||
this._correctLine.remember('_paintHandler').drawCircles();
|
||||
this._correctLine.plot(this._correctLine.array().valueOf());
|
||||
this._rescaleDrawPoints();
|
||||
}
|
||||
|
||||
_startEdit() {
|
||||
this._frame = window.cvat.player.frames.current;
|
||||
let strokeWidth = this._data.type === 'points' ? 0 : STROKE_WIDTH / this._scale;
|
||||
|
||||
// Draw copy of original shape
|
||||
if (this._data.type === 'polygon') {
|
||||
this._originalShape = this._frameContent.polygon(this._data.points);
|
||||
}
|
||||
else {
|
||||
this._originalShape = this._frameContent.polyline(this._data.points);
|
||||
}
|
||||
|
||||
this._originalShape.attr({
|
||||
'stroke-width': strokeWidth,
|
||||
'stroke': 'white',
|
||||
'fill': 'none',
|
||||
});
|
||||
|
||||
// Create the correct line
|
||||
this._correctLine = this._frameContent.polyline().draw({snapToGrid: 0.1}).attr({
|
||||
'stroke-width': strokeWidth / 2,
|
||||
'fill': 'none',
|
||||
'stroke': 'red',
|
||||
}).on('mouseover', () => false);
|
||||
|
||||
|
||||
// Add points to original shape
|
||||
let pointRadius = POINT_RADIUS / this._scale;
|
||||
this._originalShapePointsGroup = this._frameContent.group();
|
||||
for (let point of PolyShapeModel.convertStringToNumberArray(this._data.points)) {
|
||||
let uiPoint = this._originalShapePointsGroup.circle(pointRadius * 2)
|
||||
.move(point.x - pointRadius, point.y - pointRadius)
|
||||
.attr({
|
||||
'stroke-width': strokeWidth,
|
||||
'stroke': 'black',
|
||||
'fill': 'white',
|
||||
'z_order': Number.MAX_SAFE_INTEGER,
|
||||
});
|
||||
this._originalShapePoints.push(uiPoint);
|
||||
}
|
||||
|
||||
|
||||
const [x, y] = this._data.startPoint
|
||||
.split(',').map((el) => +el);
|
||||
let prevPoint = {
|
||||
x,
|
||||
y,
|
||||
};
|
||||
|
||||
// draw and remove initial point just to initialize data structures
|
||||
this._correctLine.draw('point', this._data.event);
|
||||
this._correctLine.draw('undo');
|
||||
|
||||
this._addRawPoint(x, y);
|
||||
|
||||
this._frameContent.on('mousemove.polyshapeEditor', (e) => {
|
||||
if (e.shiftKey && this._data.type !== 'points') {
|
||||
const delta = Math.sqrt(Math.pow(e.clientX - prevPoint.x, 2)
|
||||
+ Math.pow(e.clientY - prevPoint.y, 2));
|
||||
const deltaTreshold = 15;
|
||||
if (delta > deltaTreshold) {
|
||||
this._correctLine.draw('point', e);
|
||||
prevPoint = {
|
||||
x: e.clientX,
|
||||
y: e.clientY
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._frameContent.on('contextmenu.polyshapeEditor', (e) => {
|
||||
if (PolyShapeModel.convertStringToNumberArray(this._correctLine.attr('points')).length > 2) {
|
||||
this._correctLine.draw('undo');
|
||||
if (this._borderSticker) {
|
||||
this._borderSticker.reset();
|
||||
}
|
||||
} else {
|
||||
// Finish without points argument is just cancel
|
||||
this._controller.finish();
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
this._correctLine.on('drawpoint', (e) => {
|
||||
prevPoint = {
|
||||
x: e.detail.event.clientX,
|
||||
y: e.detail.event.clientY
|
||||
};
|
||||
|
||||
this._rescaleDrawPoints();
|
||||
if (this._borderSticker) {
|
||||
this._borderSticker.reset();
|
||||
}
|
||||
});
|
||||
|
||||
this._correctLine.on('drawstart', () => this._rescaleDrawPoints());
|
||||
|
||||
|
||||
for (let instance of this._originalShapePoints) {
|
||||
instance.on('mouseover', () => {
|
||||
instance.attr('stroke-width', STROKE_WIDTH * 2 / this._scale);
|
||||
}).on('mouseout', () => {
|
||||
instance.attr('stroke-width', STROKE_WIDTH / this._scale);
|
||||
}).on('mousedown', (e) => {
|
||||
if (e.which !== 1) {
|
||||
return;
|
||||
}
|
||||
let currentPoints = PolyShapeModel.convertStringToNumberArray(this._data.points);
|
||||
// replace the latest point from the event
|
||||
// (which has not precise coordinates, to precise coordinates)
|
||||
let correctPoints = this._correctLine
|
||||
.attr('points')
|
||||
.split(/\s/)
|
||||
.slice(0, -1);
|
||||
correctPoints = correctPoints.concat([`${instance.attr('cx')},${instance.attr('cy')}`]).join(' ');
|
||||
correctPoints = PolyShapeModel.convertStringToNumberArray(correctPoints);
|
||||
|
||||
let resultPoints = [];
|
||||
|
||||
if (this._data.type === 'polygon') {
|
||||
let startPtIdx = this._data.start;
|
||||
let stopPtIdx = $(instance.node).index();
|
||||
let offset = this._findMinCircleDistance(currentPoints, currentPoints[startPtIdx], currentPoints[stopPtIdx]);
|
||||
|
||||
if (!offset) {
|
||||
currentPoints = this._resortPoints(currentPoints, currentPoints[startPtIdx], currentPoints[stopPtIdx]);
|
||||
resultPoints.push(...correctPoints.slice(0, -2));
|
||||
resultPoints.push(...currentPoints);
|
||||
}
|
||||
else {
|
||||
resultPoints.push(...correctPoints);
|
||||
if (offset < 0) {
|
||||
resultPoints = resultPoints.reverse();
|
||||
currentPoints = this._resortPoints(currentPoints, currentPoints[startPtIdx], currentPoints[stopPtIdx]);
|
||||
}
|
||||
else {
|
||||
currentPoints = this._resortPoints(currentPoints, currentPoints[stopPtIdx], currentPoints[startPtIdx]);
|
||||
}
|
||||
|
||||
resultPoints.push(...currentPoints);
|
||||
}
|
||||
}
|
||||
else if (this._data.type === 'polyline') {
|
||||
let startPtIdx = this._data.start;
|
||||
let stopPtIdx = $(instance.node).index();
|
||||
|
||||
if (startPtIdx === stopPtIdx) {
|
||||
resultPoints.push(...correctPoints.slice(1, -1).reverse());
|
||||
resultPoints.push(...currentPoints);
|
||||
}
|
||||
else {
|
||||
if (startPtIdx > stopPtIdx) {
|
||||
if (startPtIdx < currentPoints.length - 1) {
|
||||
resultPoints.push(...currentPoints.slice(startPtIdx + 1).reverse());
|
||||
}
|
||||
resultPoints.push(...correctPoints.slice(0, -1));
|
||||
if (stopPtIdx > 0) {
|
||||
resultPoints.push(...currentPoints.slice(0, stopPtIdx).reverse());
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (startPtIdx > 0) {
|
||||
resultPoints.push(...currentPoints.slice(0, startPtIdx));
|
||||
}
|
||||
resultPoints.push(...correctPoints.slice(0, -1));
|
||||
if (stopPtIdx < currentPoints.length) {
|
||||
resultPoints.push(...currentPoints.slice(stopPtIdx + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
resultPoints.push(...currentPoints);
|
||||
resultPoints.push(...correctPoints.slice(1, -1).reverse());
|
||||
}
|
||||
|
||||
this._correctLine.draw('cancel');
|
||||
this._controller.finish(PolyShapeModel.convertNumberArrayToString(resultPoints));
|
||||
});
|
||||
}
|
||||
|
||||
this._commonBordersCheckbox.css('display', '').trigger('change.shapeEditor');
|
||||
this._commonBordersCheckbox.parent().css('display', '');
|
||||
$('body').on('keydown.shapeEditor', (e) => {
|
||||
if (e.ctrlKey && e.keyCode === 17) {
|
||||
this._commonBordersCheckbox.prop('checked', !this._borderSticker);
|
||||
this._commonBordersCheckbox.trigger('change.shapeEditor');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_endEdit() {
|
||||
for (let uiPoint of this._originalShapePoints) {
|
||||
uiPoint.off();
|
||||
uiPoint.remove();
|
||||
}
|
||||
|
||||
this._originalShapePoints = [];
|
||||
this._originalShapePointsGroup.remove();
|
||||
this._originalShapePointsGroup = null;
|
||||
this._originalShape.remove();
|
||||
this._originalShape = null;
|
||||
this._correctLine.off('drawstart');
|
||||
this._correctLine.off('drawpoint');
|
||||
this._correctLine.draw('cancel');
|
||||
this._correctLine.remove();
|
||||
this._correctLine = null;
|
||||
this._data = null;
|
||||
|
||||
this._frameContent.off('mousemove.polyshapeEditor');
|
||||
this._frameContent.off('mousedown.polyshapeEditor');
|
||||
this._frameContent.off('contextmenu.polyshapeEditor');
|
||||
|
||||
$('body').off('keydown.shapeEditor');
|
||||
this._commonBordersCheckbox.css('display', 'none');
|
||||
this._commonBordersCheckbox.parent().css('display', 'none');
|
||||
if (this._borderSticker) {
|
||||
this._borderSticker.disable();
|
||||
this._borderSticker = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onPolyshapeEditorUpdate(model) {
|
||||
if (model.active && !this._data) {
|
||||
this._data = model.data;
|
||||
this._startEdit();
|
||||
}
|
||||
else if (!model.active) {
|
||||
this._endEdit();
|
||||
}
|
||||
}
|
||||
|
||||
onPlayerUpdate(player) {
|
||||
let scale = player.geometry.scale;
|
||||
if (this._scale != scale) {
|
||||
this._scale = scale;
|
||||
|
||||
if (this._borderSticker) {
|
||||
this._borderSticker.scale(this._scale);
|
||||
}
|
||||
|
||||
let strokeWidth = this._data && this._data.type === 'points' ? 0 : STROKE_WIDTH / this._scale;
|
||||
let pointRadius = POINT_RADIUS / this._scale;
|
||||
|
||||
if (this._originalShape) {
|
||||
this._originalShape.attr('stroke-width', strokeWidth);
|
||||
}
|
||||
|
||||
if (this._correctLine) {
|
||||
this._correctLine.attr('stroke-width', strokeWidth / 2);
|
||||
}
|
||||
|
||||
for (let uiPoint of this._originalShapePoints) {
|
||||
uiPoint.attr('stroke-width', strokeWidth);
|
||||
uiPoint.radius(pointRadius);
|
||||
}
|
||||
|
||||
this._rescaleDrawPoints();
|
||||
}
|
||||
|
||||
// Abort if frame have been changed
|
||||
if (player.frames.current != this._frame && this._data) {
|
||||
this._controller.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,549 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
LabelsInfo:false
|
||||
Listener:false
|
||||
PlayerModel:false
|
||||
*/
|
||||
|
||||
const tests = [];
|
||||
const jobData = {
|
||||
url: 'http://localhost:7000/api/v1/jobs/3',
|
||||
id: 3,
|
||||
assignee: null,
|
||||
status: 'annotation',
|
||||
start_frame: 0,
|
||||
stop_frame: 7,
|
||||
task_id: 3,
|
||||
};
|
||||
|
||||
window.cvat = {
|
||||
player: {
|
||||
frames: {
|
||||
start: jobData.start_frame,
|
||||
stop: jobData.stop_frame,
|
||||
current: 0,
|
||||
},
|
||||
geometry: {},
|
||||
},
|
||||
translate: {},
|
||||
};
|
||||
|
||||
const taskData = {
|
||||
url: 'http://localhost:7000/api/v1/tasks/3',
|
||||
id: 3,
|
||||
name: 'QUnitTests',
|
||||
size: 8,
|
||||
mode: 'annotation',
|
||||
owner: 1,
|
||||
assignee: null,
|
||||
bug_tracker: '',
|
||||
created_date: '2019-03-27T16:19:24.525806+03:00',
|
||||
updated_date: '2019-03-27T16:19:24.525858+03:00',
|
||||
overlap: 0,
|
||||
segment_size: 0,
|
||||
z_order: false,
|
||||
flipped: false,
|
||||
status: 'annotation',
|
||||
labels: [
|
||||
{
|
||||
id: 17,
|
||||
name: 'bicycle',
|
||||
attributes: [
|
||||
{
|
||||
id: 28,
|
||||
name: 'driver',
|
||||
mutable: false,
|
||||
input_type: 'radio',
|
||||
default_value: 'man',
|
||||
values: [
|
||||
'man',
|
||||
'woman',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 29,
|
||||
name: 'sport',
|
||||
mutable: true,
|
||||
input_type: 'checkbox',
|
||||
default_value: 'false',
|
||||
values: [
|
||||
'false',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: 'car',
|
||||
attributes: [
|
||||
{
|
||||
id: 25,
|
||||
name: 'model',
|
||||
mutable: false,
|
||||
input_type: 'select',
|
||||
default_value: '__undefined__',
|
||||
values: [
|
||||
'__undefined__',
|
||||
'bmw',
|
||||
'mazda',
|
||||
'suzuki',
|
||||
'kia',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 26,
|
||||
name: 'driver',
|
||||
mutable: false,
|
||||
input_type: 'select',
|
||||
default_value: '__undefined__',
|
||||
values: [
|
||||
'__undefined__',
|
||||
'man',
|
||||
'woman',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 27,
|
||||
name: 'parked',
|
||||
mutable: true,
|
||||
input_type: 'checkbox',
|
||||
default_value: 'true',
|
||||
values: [
|
||||
'true',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: 'face',
|
||||
attributes: [
|
||||
{
|
||||
id: 21,
|
||||
name: 'age',
|
||||
mutable: false,
|
||||
input_type: 'select',
|
||||
default_value: '__undefined__',
|
||||
values: [
|
||||
'__undefined__',
|
||||
'skip',
|
||||
'baby (0-5)',
|
||||
'child (6-12)',
|
||||
'adolescent (13-19)',
|
||||
'adult (20-45)',
|
||||
'middle-age (46-64)',
|
||||
'old (65-)',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
name: 'glass',
|
||||
mutable: false,
|
||||
input_type: 'select',
|
||||
default_value: '__undefined__',
|
||||
values: [
|
||||
'__undefined__',
|
||||
'skip',
|
||||
'no',
|
||||
'sunglass',
|
||||
'transparent',
|
||||
'other',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 23,
|
||||
name: 'beard',
|
||||
mutable: false,
|
||||
input_type: 'select',
|
||||
default_value: '__undefined__',
|
||||
values: [
|
||||
'__undefined__',
|
||||
'skip',
|
||||
'no',
|
||||
'yes',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 24,
|
||||
name: 'race',
|
||||
mutable: false,
|
||||
input_type: 'select',
|
||||
default_value: '__undefined__',
|
||||
values: [
|
||||
'__undefined__',
|
||||
'skip',
|
||||
'asian',
|
||||
'black',
|
||||
'caucasian',
|
||||
'other',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
name: 'motorcycle',
|
||||
attributes: [
|
||||
{
|
||||
id: 30,
|
||||
name: 'model',
|
||||
mutable: false,
|
||||
input_type: 'text',
|
||||
default_value: 'unknown',
|
||||
values: [
|
||||
'unknown',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: 'person, pedestrian',
|
||||
attributes: [
|
||||
{
|
||||
id: 16,
|
||||
name: 'action',
|
||||
mutable: true,
|
||||
input_type: 'select',
|
||||
default_value: '__undefined__',
|
||||
values: [
|
||||
'__undefined__',
|
||||
'sitting',
|
||||
'raising_hand',
|
||||
'standing',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
name: 'age',
|
||||
mutable: false,
|
||||
input_type: 'number',
|
||||
default_value: '1',
|
||||
values: [
|
||||
'1',
|
||||
'100',
|
||||
'1',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
name: 'gender',
|
||||
mutable: false,
|
||||
input_type: 'select',
|
||||
default_value: 'male',
|
||||
values: [
|
||||
'male',
|
||||
'female',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
name: 'false positive',
|
||||
mutable: false,
|
||||
input_type: 'checkbox',
|
||||
default_value: 'false',
|
||||
values: [
|
||||
'false',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
name: 'clother',
|
||||
mutable: true,
|
||||
input_type: 'text',
|
||||
default_value: 'non, initialized',
|
||||
values: [
|
||||
'non, initialized',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
name: 'road',
|
||||
attributes: [],
|
||||
},
|
||||
],
|
||||
segments: [
|
||||
{
|
||||
start_frame: 0,
|
||||
stop_frame: 7,
|
||||
jobs: [
|
||||
{
|
||||
url: 'http://localhost:7000/api/v1/jobs/3',
|
||||
id: 3,
|
||||
assignee: null,
|
||||
status: 'annotation',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
image_quality: 95,
|
||||
};
|
||||
|
||||
|
||||
function makeLabelsInfo() {
|
||||
return new LabelsInfo(taskData.labels);
|
||||
}
|
||||
|
||||
function makePlayerModel() {
|
||||
const dummyPlayerGeometry = {
|
||||
width: 800,
|
||||
height: 600,
|
||||
left: 10,
|
||||
top: 10,
|
||||
};
|
||||
|
||||
return new PlayerModel(taskData, dummyPlayerGeometry);
|
||||
}
|
||||
|
||||
// Run all tests
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
for (const test of tests) {
|
||||
test();
|
||||
}
|
||||
});
|
||||
|
||||
tests.push(() => {
|
||||
let labelsInfo = null;
|
||||
QUnit.module('LabelsInfo', {
|
||||
before() {
|
||||
labelsInfo = makeLabelsInfo();
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test('labelIdOf', (assert) => {
|
||||
assert.equal(labelsInfo.labelIdOf('bicycle'), 17);
|
||||
assert.equal(labelsInfo.labelIdOf('car'), 16);
|
||||
assert.equal(labelsInfo.labelIdOf('face'), 15);
|
||||
assert.equal(labelsInfo.labelIdOf('motorcycle'), 18);
|
||||
assert.equal(labelsInfo.labelIdOf('person, pedestrian'), 14);
|
||||
assert.equal(labelsInfo.labelIdOf('road'), 19);
|
||||
assert.throws(labelsInfo.labelIdOf.bind(labelsInfo, 'unknown_label'));
|
||||
});
|
||||
|
||||
QUnit.test('attrIdOf', (assert) => {
|
||||
assert.equal(labelsInfo.attrIdOf(14, 'action'), labelsInfo.attrIdOf('14', 'action'));
|
||||
assert.equal(labelsInfo.attrIdOf(18, 'model'), 30);
|
||||
assert.equal(labelsInfo.attrIdOf(15, 'age'), 21);
|
||||
assert.throws(labelsInfo.attrIdOf.bind(labelsInfo, 15, 'unknown_attribute'));
|
||||
assert.throws(labelsInfo.attrIdOf.bind(labelsInfo, 99, 'age'));
|
||||
assert.throws(labelsInfo.attrIdOf.bind(labelsInfo, undefined, 'driver'));
|
||||
assert.throws(labelsInfo.attrIdOf.bind(labelsInfo, '15', undefined));
|
||||
});
|
||||
|
||||
QUnit.test('normalize', (assert) => {
|
||||
assert.equal(LabelsInfo.normalize('checkbox', 'false'), false);
|
||||
assert.equal(LabelsInfo.normalize('checkbox', 'false,true'), true);
|
||||
assert.equal(LabelsInfo.normalize('checkbox', '0'), false);
|
||||
assert.equal(LabelsInfo.normalize('checkbox', false), false);
|
||||
assert.equal(LabelsInfo.normalize('checkbox', 'abrakadabra'), true);
|
||||
assert.equal(LabelsInfo.normalize('select', 'value1'), 'value1');
|
||||
assert.equal(LabelsInfo.normalize('text', 'value1,together value2 and 3'), 'value1,together value2 and 3');
|
||||
assert.equal(LabelsInfo.normalize('radio', 'value'), 'value');
|
||||
assert.equal(LabelsInfo.normalize('number', '1'), 1);
|
||||
assert.equal(LabelsInfo.normalize('number', 1), 1);
|
||||
assert.throws(LabelsInfo.normalize.bind(LabelsInfo, 'number', 'abrakadabra'));
|
||||
});
|
||||
|
||||
QUnit.test('labels', (assert) => {
|
||||
const expected = {
|
||||
14: 'person, pedestrian',
|
||||
15: 'face',
|
||||
16: 'car',
|
||||
17: 'bicycle',
|
||||
18: 'motorcycle',
|
||||
19: 'road',
|
||||
};
|
||||
|
||||
assert.deepEqual(labelsInfo.labels(), expected);
|
||||
});
|
||||
|
||||
QUnit.test('attributes', (assert) => {
|
||||
const expected = {
|
||||
16: 'action',
|
||||
17: 'age',
|
||||
18: 'gender',
|
||||
19: 'false positive',
|
||||
20: 'clother',
|
||||
21: 'age',
|
||||
22: 'glass',
|
||||
23: 'beard',
|
||||
24: 'race',
|
||||
25: 'model',
|
||||
26: 'driver',
|
||||
27: 'parked',
|
||||
28: 'driver',
|
||||
29: 'sport',
|
||||
30: 'model',
|
||||
};
|
||||
|
||||
assert.deepEqual(labelsInfo.attributes(), expected);
|
||||
});
|
||||
|
||||
QUnit.test('labelAttributes', (assert) => {
|
||||
assert.deepEqual(labelsInfo.labelAttributes(14), {
|
||||
16: 'action',
|
||||
17: 'age',
|
||||
18: 'gender',
|
||||
19: 'false positive',
|
||||
20: 'clother',
|
||||
});
|
||||
|
||||
assert.deepEqual(labelsInfo.labelAttributes(15), {
|
||||
21: 'age',
|
||||
22: 'glass',
|
||||
23: 'beard',
|
||||
24: 'race',
|
||||
});
|
||||
|
||||
assert.deepEqual(labelsInfo.labelAttributes(19), {});
|
||||
assert.deepEqual(labelsInfo.labelAttributes(14), labelsInfo.labelAttributes('14'));
|
||||
assert.throws(labelsInfo.labelAttributes.bind(labelsInfo, 100));
|
||||
assert.throws(labelsInfo.labelAttributes.bind(labelsInfo));
|
||||
assert.throws(labelsInfo.labelAttributes.bind(labelsInfo, null));
|
||||
});
|
||||
|
||||
QUnit.test('attrInfo', (assert) => {
|
||||
assert.deepEqual(labelsInfo.attrInfo(21), {
|
||||
mutable: false,
|
||||
type: 'select',
|
||||
name: 'age',
|
||||
values: [
|
||||
'__undefined__',
|
||||
'skip',
|
||||
'baby (0-5)',
|
||||
'child (6-12)',
|
||||
'adolescent (13-19)',
|
||||
'adult (20-45)',
|
||||
'middle-age (46-64)',
|
||||
'old (65-)',
|
||||
],
|
||||
});
|
||||
|
||||
assert.deepEqual(labelsInfo.attrInfo(29), {
|
||||
mutable: true,
|
||||
type: 'checkbox',
|
||||
name: 'sport',
|
||||
values: [
|
||||
false,
|
||||
],
|
||||
});
|
||||
|
||||
assert.deepEqual(labelsInfo.attrInfo(23), labelsInfo.attrInfo('23'));
|
||||
assert.throws(labelsInfo.attrInfo.bind(labelsInfo, 100));
|
||||
assert.throws(labelsInfo.attrInfo.bind(labelsInfo));
|
||||
assert.throws(labelsInfo.attrInfo.bind(labelsInfo, 'clother'));
|
||||
assert.throws(labelsInfo.attrInfo.bind(labelsInfo, null));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
tests.push(() => {
|
||||
QUnit.module('Listener');
|
||||
|
||||
QUnit.test('subscribe', (assert) => {
|
||||
const listenerInterface = new Listener('onUpdate', () => {});
|
||||
const dummyListener1 = {
|
||||
onUpdate() {},
|
||||
};
|
||||
const dummyListener2 = {
|
||||
onUpdate: 'someProp',
|
||||
};
|
||||
const dummyListener3 = {};
|
||||
const dummyListener4 = {
|
||||
onUpdate() {},
|
||||
};
|
||||
const dummyListener5 = {
|
||||
onUpdate() {},
|
||||
};
|
||||
|
||||
listenerInterface.subscribe(dummyListener1); // no exceptions, listener added
|
||||
listenerInterface.subscribe(dummyListener4); // no exceptions, listener added
|
||||
listenerInterface.subscribe(dummyListener5); // no exceptions, listener added
|
||||
|
||||
assert.throws(listenerInterface.subscribe.bind(listenerInterface, dummyListener2));
|
||||
assert.throws(listenerInterface.subscribe.bind(listenerInterface, dummyListener3));
|
||||
});
|
||||
|
||||
QUnit.test('unsubscribe', (assert) => {
|
||||
const listenerInterface = new Listener('onUpdate', () => {});
|
||||
|
||||
const dummyListener1 = {
|
||||
onUpdate() {},
|
||||
};
|
||||
|
||||
const dummyListener2 = {
|
||||
onUpdate() {},
|
||||
};
|
||||
|
||||
const dummyListener3 = {
|
||||
onUpdate() {},
|
||||
};
|
||||
|
||||
const dummyListener4 = {
|
||||
onUpdate() {},
|
||||
};
|
||||
|
||||
listenerInterface.subscribe(dummyListener1);
|
||||
listenerInterface.subscribe(dummyListener2);
|
||||
listenerInterface.subscribe(dummyListener3);
|
||||
listenerInterface.subscribe(dummyListener4);
|
||||
|
||||
listenerInterface.unsubscribe(dummyListener2);
|
||||
listenerInterface.unsubscribe(dummyListener4);
|
||||
|
||||
assert.throws(listenerInterface.unsubscribe.bind(listenerInterface, null));
|
||||
assert.throws(listenerInterface.unsubscribe.bind(listenerInterface));
|
||||
|
||||
listenerInterface.unsubscribe(dummyListener1);
|
||||
listenerInterface.unsubscribe(dummyListener3);
|
||||
});
|
||||
|
||||
QUnit.test('unsubscribeAll', (assert) => {
|
||||
const listenerInterface = new Listener('onUpdate', () => {});
|
||||
const dummyListener1 = {
|
||||
onUpdate() {},
|
||||
};
|
||||
|
||||
const dummyListener2 = {
|
||||
onUpdate() {},
|
||||
};
|
||||
|
||||
listenerInterface.subscribe(dummyListener1);
|
||||
listenerInterface.subscribe(dummyListener2);
|
||||
|
||||
listenerInterface.unsubscribeAll();
|
||||
|
||||
assert.expect(0);
|
||||
});
|
||||
});
|
||||
|
||||
tests.push(() => {
|
||||
let playerModel = null;
|
||||
QUnit.module('PlayerModel', {
|
||||
before() {
|
||||
playerModel = makePlayerModel();
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test('scale', (assert) => {
|
||||
// Scale when player is not ready
|
||||
assert.expect(0);
|
||||
playerModel.scale(20, 20, 1);
|
||||
});
|
||||
|
||||
|
||||
QUnit.test('fit', (assert) => {
|
||||
// Fit when player is not ready
|
||||
assert.expect(0);
|
||||
playerModel.fit();
|
||||
});
|
||||
});
|
||||
|
||||
// TODO TESTS FOR ANNOTATIONS PARSER
|
||||
@ -1,11 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* exported encodeFilePathToURI */
|
||||
|
||||
function encodeFilePathToURI(path) {
|
||||
return path.split('/').map(x => encodeURIComponent(x)).join('/');
|
||||
}
|
||||
@ -1,563 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* exported ShapeBufferModel ShapeBufferController ShapeBufferView */
|
||||
|
||||
/* global
|
||||
AREA_TRESHOLD:false
|
||||
userConfirm:false
|
||||
Listener:false
|
||||
Logger:false
|
||||
Mousetrap:false
|
||||
POINT_RADIUS:false
|
||||
PolyShapeModel:false
|
||||
STROKE_WIDTH:false
|
||||
SVG:false
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
class ShapeBufferModel extends Listener {
|
||||
constructor(collection) {
|
||||
super('onShapeBufferUpdate', () => this);
|
||||
this._collection = collection;
|
||||
this._pasteMode = false;
|
||||
this._propagateFrames = 50;
|
||||
this._shape = {
|
||||
type: null,
|
||||
mode: null,
|
||||
position: null,
|
||||
attributes: null,
|
||||
label: null,
|
||||
clear: function() {
|
||||
this.type = null;
|
||||
this.mode = null;
|
||||
this.position = null;
|
||||
this.attributes = null;
|
||||
this.label = null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_startPaste() {
|
||||
if (!this._pasteMode && this._shape.type) {
|
||||
if (!window.cvat.mode) {
|
||||
this._collection.resetActive();
|
||||
window.cvat.mode = 'paste';
|
||||
this._pasteMode = true;
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_cancelPaste() {
|
||||
if (this._pasteMode) {
|
||||
if (window.cvat.mode === 'paste') {
|
||||
window.cvat.mode = null;
|
||||
this._pasteMode = false;
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_makeObject(box, points, isTracked) {
|
||||
if (!this._shape.type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let attributes = [];
|
||||
let object = {};
|
||||
|
||||
for (let attrId in this._shape.attributes) {
|
||||
attributes.push({
|
||||
id: attrId,
|
||||
value: this._shape.attributes[attrId].value,
|
||||
});
|
||||
}
|
||||
|
||||
object.label_id = this._shape.label;
|
||||
object.group = 0;
|
||||
object.frame = window.cvat.player.frames.current;
|
||||
object.attributes = attributes;
|
||||
|
||||
if (this._shape.type === 'box') {
|
||||
const position = {
|
||||
xtl: box.xtl,
|
||||
ytl: box.ytl,
|
||||
xbr: box.xbr,
|
||||
ybr: box.ybr,
|
||||
occluded: this._shape.position.occluded,
|
||||
frame: window.cvat.player.frames.current,
|
||||
z_order: this._collection.zOrder(window.cvat.player.frames.current).max,
|
||||
};
|
||||
|
||||
if (isTracked) {
|
||||
object.shapes = [];
|
||||
object.shapes.push(Object.assign(position, {
|
||||
outside: false,
|
||||
attributes: [],
|
||||
}));
|
||||
} else {
|
||||
Object.assign(object, position);
|
||||
}
|
||||
} else {
|
||||
const position = {};
|
||||
position.points = points;
|
||||
position.occluded = this._shape.position.occluded;
|
||||
position.frame = window.cvat.player.frames.current;
|
||||
position.z_order = this._collection.zOrder(position.frame).max;
|
||||
|
||||
Object.assign(object, position);
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
switchPaste() {
|
||||
if (this._pasteMode) {
|
||||
this._cancelPaste();
|
||||
}
|
||||
else {
|
||||
this._startPaste();
|
||||
}
|
||||
}
|
||||
|
||||
copyToBuffer() {
|
||||
let activeShape = this._collection.activeShape;
|
||||
if (activeShape) {
|
||||
Logger.addEvent(Logger.EventType.copyObject, {
|
||||
count: 1,
|
||||
});
|
||||
let interpolation = activeShape.interpolate(window.cvat.player.frames.current);
|
||||
if (!interpolation.position.outsided) {
|
||||
this._shape.type = activeShape.type.split('_')[1];
|
||||
this._shape.mode = activeShape.type.split('_')[0];
|
||||
this._shape.label = activeShape.label;
|
||||
this._shape.attributes = interpolation.attributes;
|
||||
this._shape.position = interpolation.position;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pasteToFrame(box, polyPoints) {
|
||||
let object = this._makeObject(box, polyPoints, this._shape.mode === 'interpolation');
|
||||
|
||||
if (object) {
|
||||
if (this._shape.type === 'cuboid'
|
||||
&& !CuboidModel.isWithinFrame(PolyShapeModel.convertStringToNumberArray(polyPoints))) {
|
||||
return
|
||||
}
|
||||
|
||||
Logger.addEvent(Logger.EventType.pasteObject);
|
||||
if (this._shape.type === 'box') {
|
||||
this._collection.add(object, `${this._shape.mode}_${this._shape.type}`);
|
||||
}
|
||||
else {
|
||||
this._collection.add(object, `annotation_${this._shape.type}`);
|
||||
}
|
||||
|
||||
// Undo/redo code
|
||||
let model = this._collection.shapes.slice(-1)[0];
|
||||
window.cvat.addAction('Paste Object', () => {
|
||||
model.removed = true;
|
||||
model.unsubscribe(this._collection);
|
||||
}, () => {
|
||||
model.subscribe(this._collection);
|
||||
model.removed = false;
|
||||
}, window.cvat.player.frames.current);
|
||||
// End of undo/redo code
|
||||
|
||||
this._collection.update();
|
||||
}
|
||||
}
|
||||
|
||||
propagateToFrames() {
|
||||
let numOfFrames = this._propagateFrames;
|
||||
if (this._shape.type && Number.isInteger(numOfFrames)) {
|
||||
let object = null;
|
||||
if (this._shape.type === 'box') {
|
||||
let box = {
|
||||
xtl: this._shape.position.xtl,
|
||||
ytl: this._shape.position.ytl,
|
||||
xbr: this._shape.position.xbr,
|
||||
ybr: this._shape.position.ybr,
|
||||
};
|
||||
object = this._makeObject(box, null, false);
|
||||
} else {
|
||||
object = this._makeObject(null, this._shape.position.points, false);
|
||||
}
|
||||
|
||||
if (object) {
|
||||
Logger.addEvent(Logger.EventType.propagateObject, {
|
||||
count: numOfFrames,
|
||||
});
|
||||
|
||||
let imageSizes = window.cvat.job.images;
|
||||
let startFrame = window.cvat.player.frames.start;
|
||||
let originalImageSize = imageSizes.frames[object.frame - startFrame] || imageSizes.frames[0];
|
||||
|
||||
// Getting normalized coordinates [0..1]
|
||||
let normalized = {};
|
||||
if (this._shape.type === 'box') {
|
||||
normalized.xtl = object.xtl / originalImageSize.width;
|
||||
normalized.ytl = object.ytl / originalImageSize.height;
|
||||
normalized.xbr = object.xbr / originalImageSize.width;
|
||||
normalized.ybr = object.ybr / originalImageSize.height;
|
||||
}
|
||||
else {
|
||||
normalized.points = [];
|
||||
for (let point of PolyShapeModel.convertStringToNumberArray(object.points)) {
|
||||
normalized.points.push({
|
||||
x: point.x / originalImageSize.width,
|
||||
y: point.y / originalImageSize.height,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let addedObjects = [];
|
||||
while (numOfFrames > 0 && (object.frame + 1 <= window.cvat.player.frames.stop)) {
|
||||
object.frame ++;
|
||||
numOfFrames --;
|
||||
|
||||
object.z_order = this._collection.zOrder(object.frame).max;
|
||||
let imageSize = imageSizes.frames[object.frame - startFrame] || imageSizes.frames[0];
|
||||
let position = {};
|
||||
if (this._shape.type === 'box') {
|
||||
position.xtl = normalized.xtl * imageSize.width;
|
||||
position.ytl = normalized.ytl * imageSize.height;
|
||||
position.xbr = normalized.xbr * imageSize.width;
|
||||
position.ybr = normalized.ybr * imageSize.height;
|
||||
}
|
||||
else {
|
||||
position.points = [];
|
||||
for (let point of normalized.points) {
|
||||
position.points.push({
|
||||
x: point.x * imageSize.width,
|
||||
y: point.y * imageSize.height,
|
||||
});
|
||||
}
|
||||
position.points = PolyShapeModel.convertNumberArrayToString(position.points);
|
||||
}
|
||||
Object.assign(object, position);
|
||||
this._collection.add(object, `annotation_${this._shape.type}`);
|
||||
addedObjects.push(this._collection.shapes.slice(-1)[0]);
|
||||
}
|
||||
|
||||
if (addedObjects.length) {
|
||||
// Undo/redo code
|
||||
window.cvat.addAction('Propagate Object', () => {
|
||||
for (let object of addedObjects) {
|
||||
object.removed = true;
|
||||
object.unsubscribe(this._collection);
|
||||
}
|
||||
}, () => {
|
||||
for (let object of addedObjects) {
|
||||
object.removed = false;
|
||||
object.subscribe(this._collection);
|
||||
}
|
||||
}, window.cvat.player.frames.current);
|
||||
// End of undo/redo code
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get pasteMode() {
|
||||
return this._pasteMode;
|
||||
}
|
||||
|
||||
get shape() {
|
||||
return this._shape;
|
||||
}
|
||||
|
||||
set propagateFrames(value) {
|
||||
this._propagateFrames = value;
|
||||
}
|
||||
|
||||
get propagateFrames() {
|
||||
return this._propagateFrames;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ShapeBufferController {
|
||||
constructor(model) {
|
||||
this._model = model;
|
||||
setupBufferShortkeys.call(this);
|
||||
|
||||
function setupBufferShortkeys() {
|
||||
let copyHandler = Logger.shortkeyLogDecorator(function() {
|
||||
this._model.copyToBuffer();
|
||||
}.bind(this));
|
||||
|
||||
let switchHandler = Logger.shortkeyLogDecorator(function() {
|
||||
this._model.switchPaste();
|
||||
}.bind(this));
|
||||
|
||||
let propagateDialogShowed = false;
|
||||
let propagateHandler = Logger.shortkeyLogDecorator(function() {
|
||||
if (!propagateDialogShowed) {
|
||||
blurAllElements();
|
||||
if (this._model.copyToBuffer()) {
|
||||
let curFrame = window.cvat.player.frames.current;
|
||||
let startFrame = window.cvat.player.frames.start;
|
||||
let endFrame = Math.min(window.cvat.player.frames.stop, curFrame + this._model.propagateFrames);
|
||||
let imageSizes = window.cvat.job.images;
|
||||
|
||||
let message = `Propagate up to ${endFrame} frame. `;
|
||||
let refSize = imageSizes.frames[curFrame - startFrame] || imageSizes.frames[0];
|
||||
for (let _frame = curFrame + 1; _frame <= endFrame; _frame ++) {
|
||||
let size = imageSizes.frames[_frame - startFrame] || imageSizes.frames[0];
|
||||
if ((size.width != refSize.width) || (size.height != refSize.height) ) {
|
||||
message += 'Some covered frames have another resolution. Shapes in them can differ from reference. ';
|
||||
break;
|
||||
}
|
||||
}
|
||||
message += 'Are you sure?';
|
||||
|
||||
propagateDialogShowed = true;
|
||||
userConfirm(message, () => {
|
||||
this._model.propagateToFrames();
|
||||
propagateDialogShowed = false;
|
||||
}, () => propagateDialogShowed = false);
|
||||
}
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
let shortkeys = window.cvat.config.shortkeys;
|
||||
Mousetrap.bind(shortkeys["copy_shape"].value, copyHandler, 'keydown');
|
||||
Mousetrap.bind(shortkeys["propagate_shape"].value, propagateHandler, 'keydown');
|
||||
Mousetrap.bind(shortkeys["switch_paste"].value, switchHandler, 'keydown');
|
||||
}
|
||||
}
|
||||
|
||||
pasteToFrame(e, bbRect, polyPoints) {
|
||||
if (this._model.pasteMode) {
|
||||
if (bbRect || polyPoints) {
|
||||
this._model.pasteToFrame(bbRect, polyPoints);
|
||||
}
|
||||
|
||||
if (!e.ctrlKey) {
|
||||
this._model.switchPaste();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set propagateFrames(value) {
|
||||
this._model.propagateFrames = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class ShapeBufferView {
|
||||
constructor(model, controller) {
|
||||
model.subscribe(this);
|
||||
this._controller = controller;
|
||||
this._frameContent = SVG.adopt($('#frameContent')[0]);
|
||||
this._propagateFramesInput = $('#propagateFramesInput');
|
||||
this._shape = null;
|
||||
this._shapeView = null;
|
||||
this._shapeViewGroup = null;
|
||||
|
||||
this._controller.propagateFrames = +this._propagateFramesInput.prop('value');
|
||||
this._propagateFramesInput.on('change', (e) => {
|
||||
let value = Math.clamp(+e.target.value, +e.target.min, +e.target.max);
|
||||
e.target.value = value;
|
||||
this._controller.propagateFrames = value;
|
||||
});
|
||||
}
|
||||
|
||||
_drawShapeView() {
|
||||
let scale = window.cvat.player.geometry.scale;
|
||||
let points = this._shape.position.points ?
|
||||
window.cvat.translate.points.actualToCanvas(this._shape.position.points) : null;
|
||||
|
||||
switch (this._shape.type) {
|
||||
case 'box': {
|
||||
let width = this._shape.position.xbr - this._shape.position.xtl;
|
||||
let height = this._shape.position.ybr - this._shape.position.ytl;
|
||||
this._shape.position = window.cvat.translate.box.actualToCanvas(this._shape.position);
|
||||
this._shapeView = this._frameContent.rect(width, height)
|
||||
.move(this._shape.position.xtl, this._shape.position.ytl).addClass('shapeCreation').attr({
|
||||
'stroke-width': STROKE_WIDTH / scale,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'polygon':
|
||||
this._shapeView = this._frameContent.polygon(points).addClass('shapeCreation').attr({
|
||||
'stroke-width': STROKE_WIDTH / scale,
|
||||
});
|
||||
break;
|
||||
case 'cuboid':
|
||||
points = window.cvat.translate.points.canvasToActual(points);
|
||||
points = PolylineModel.convertStringToNumberArray(points);
|
||||
let view_model = new Cuboid2PointViewModel(points);
|
||||
this._shapeView = this._frameContent.polyline(points).addClass('shapeCreation').attr({
|
||||
'stroke-width': 0,
|
||||
});
|
||||
|
||||
this._shapeViewGroup = this._frameContent.cube(view_model).addClass('shapeCreation').attr({
|
||||
'stroke-width': STROKE_WIDTH / scale,
|
||||
});
|
||||
|
||||
break;
|
||||
case 'polyline':
|
||||
this._shapeView = this._frameContent.polyline(points).addClass('shapeCreation').attr({
|
||||
'stroke-width': STROKE_WIDTH / scale,
|
||||
});
|
||||
break;
|
||||
case 'points':
|
||||
this._shapeView = this._frameContent.polyline(points).addClass('shapeCreation').attr({
|
||||
'stroke-width': 0,
|
||||
});
|
||||
|
||||
this._shapeViewGroup = this._frameContent.group();
|
||||
for (let point of PolyShapeModel.convertStringToNumberArray(points)) {
|
||||
let radius = POINT_RADIUS * 2 / window.cvat.player.geometry.scale;
|
||||
let scaledStroke = STROKE_WIDTH / window.cvat.player.geometry.scale;
|
||||
this._shapeViewGroup.circle(radius).move(point.x - radius / 2, point.y - radius / 2)
|
||||
.fill('white').stroke('black').attr('stroke-width', scaledStroke).addClass('pasteTempMarker');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw Error(`Unknown shape type found: ${this._shape.type}`);
|
||||
}
|
||||
|
||||
this._shapeView.attr({
|
||||
'z_order': Number.MAX_SAFE_INTEGER,
|
||||
});
|
||||
}
|
||||
|
||||
_moveShapeView(pos) {
|
||||
let rect = this._shapeView.node.getBBox();
|
||||
|
||||
this._shapeView.move(pos.x - rect.width / 2, pos.y - rect.height / 2);
|
||||
if (this._shapeViewGroup) {
|
||||
let rect = this._shapeViewGroup.node.getBBox();
|
||||
this._shapeViewGroup.move(pos.x - rect.x - rect.width / 2, pos.y - rect.y - rect.height / 2);
|
||||
}
|
||||
}
|
||||
|
||||
_removeShapeView() {
|
||||
this._shapeView.remove();
|
||||
this._shapeView = null;
|
||||
if (this._shapeViewGroup) {
|
||||
this._shapeViewGroup.remove();
|
||||
this._shapeViewGroup = null;
|
||||
}
|
||||
}
|
||||
|
||||
_enableEvents() {
|
||||
this._frameContent.on('mousemove.buffer', (e) => {
|
||||
let pos = window.cvat.translate.point.clientToCanvas(this._frameContent.node, e.clientX, e.clientY);
|
||||
this._shapeView.style('visibility', '');
|
||||
this._moveShapeView(pos);
|
||||
});
|
||||
|
||||
this._frameContent.on('mousedown.buffer', (e) => {
|
||||
if (e.which != 1) return;
|
||||
if (this._shape.type != 'box') {
|
||||
let actualPoints = window.cvat.translate.points.canvasToActual(this._shapeView.attr('points'));
|
||||
let frameWidth = window.cvat.player.geometry.frameWidth;
|
||||
let frameHeight = window.cvat.player.geometry.frameHeight;
|
||||
|
||||
if (this.clipToFrame) {
|
||||
actualPoints = PolyShapeModel.convertStringToNumberArray(actualPoints);
|
||||
for (let point of actualPoints) {
|
||||
point.x = Math.clamp(point.x, 0, frameWidth);
|
||||
point.y = Math.clamp(point.y, 0, frameHeight);
|
||||
}
|
||||
actualPoints = PolyShapeModel.convertNumberArrayToString(actualPoints);
|
||||
}
|
||||
|
||||
// Set clamped points to a view in order to get an updated bounding box for a poly shape
|
||||
this._shapeView.attr('points', window.cvat.translate.points.actualToCanvas(actualPoints));
|
||||
|
||||
// Get an updated bounding box for check it area
|
||||
let polybox = this._shapeView.node.getBBox();
|
||||
let w = polybox.width;
|
||||
let h = polybox.height;
|
||||
let area = w * h;
|
||||
let type = this._shape.type;
|
||||
|
||||
if (area >= AREA_TRESHOLD || type === 'points' || type === 'polyline' && (w >= AREA_TRESHOLD || h >= AREA_TRESHOLD)) {
|
||||
this._controller.pasteToFrame(e, null, actualPoints);
|
||||
}
|
||||
else {
|
||||
this._controller.pasteToFrame(e, null, null);
|
||||
}
|
||||
}
|
||||
else {
|
||||
let frameWidth = window.cvat.player.geometry.frameWidth;
|
||||
let frameHeight = window.cvat.player.geometry.frameHeight;
|
||||
let rect = window.cvat.translate.box.canvasToActual(this._shapeView.node.getBBox());
|
||||
let box = {};
|
||||
box.xtl = Math.clamp(rect.x, 0, frameWidth);
|
||||
box.ytl = Math.clamp(rect.y, 0, frameHeight);
|
||||
box.xbr = Math.clamp(rect.x + rect.width, 0, frameWidth);
|
||||
box.ybr = Math.clamp(rect.y + rect.height, 0, frameHeight);
|
||||
|
||||
if ((box.xbr - box.xtl) * (box.ybr - box.ytl) >= AREA_TRESHOLD) {
|
||||
this._controller.pasteToFrame(e, box, null);
|
||||
}
|
||||
else {
|
||||
this._controller.pasteToFrame(e, null, null);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._frameContent.on('mouseleave.buffer', () => {
|
||||
this._shapeView.style('visibility', 'hidden');
|
||||
});
|
||||
}
|
||||
|
||||
_disableEvents() {
|
||||
this._frameContent.off('mousemove.buffer');
|
||||
this._frameContent.off('mousedown.buffer');
|
||||
this._frameContent.off('mouseleave.buffer');
|
||||
}
|
||||
|
||||
onShapeBufferUpdate(buffer) {
|
||||
if (buffer.pasteMode) {
|
||||
this._shape = buffer.shape;
|
||||
this._drawShapeView();
|
||||
this._enableEvents();
|
||||
}
|
||||
else {
|
||||
if (this._shapeView) {
|
||||
this._disableEvents();
|
||||
this._removeShapeView();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onBufferUpdate(buffer) {
|
||||
if (buffer.pasteMode && !this._pasteMode) {
|
||||
this._pasteMode = true;
|
||||
this._shape = buffer.shape;
|
||||
this.enableMouseEvents();
|
||||
}
|
||||
else if (!buffer.pasteMode) {
|
||||
this.disableMouseEvents();
|
||||
if (this._viewShape) {
|
||||
this._viewShape.remove();
|
||||
}
|
||||
this._viewShape = null;
|
||||
this._shape = null;
|
||||
this._pasteMode = false;
|
||||
}
|
||||
}
|
||||
|
||||
onPlayerUpdate(player) {
|
||||
if (!player.ready()) return;
|
||||
if (this._shapeView != null && this._shape.type != 'points') {
|
||||
this._shapeView.attr('stroke-width', STROKE_WIDTH / player.geometry.scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,963 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* exported ShapeCreatorModel ShapeCreatorController ShapeCreatorView */
|
||||
|
||||
/* global
|
||||
AREA_TRESHOLD:false
|
||||
drawBoxSize:false
|
||||
Listener:false
|
||||
Logger:false
|
||||
Mousetrap:false
|
||||
PolyShapeModel:false
|
||||
showMessage:false
|
||||
STROKE_WIDTH:false
|
||||
SVG:false
|
||||
BorderSticker: false
|
||||
CuboidModel:false
|
||||
Cuboid2PointViewModel:false
|
||||
*/
|
||||
|
||||
class ShapeCreatorModel extends Listener {
|
||||
constructor(shapeCollection) {
|
||||
super('onShapeCreatorUpdate', () => this);
|
||||
this._createMode = false;
|
||||
this._saveCurrent = false;
|
||||
this._defaultType = null;
|
||||
this._defaultMode = null;
|
||||
this._defaultLabel = null;
|
||||
this._currentFrame = null;
|
||||
this._createEvent = null;
|
||||
this._shapeCollection = shapeCollection;
|
||||
}
|
||||
|
||||
finish(result) {
|
||||
const data = {};
|
||||
const frame = window.cvat.player.frames.current;
|
||||
|
||||
data.label_id = this._defaultLabel;
|
||||
data.group = 0;
|
||||
data.frame = frame;
|
||||
data.occluded = false;
|
||||
data.outside = false;
|
||||
data.z_order = this._shapeCollection.zOrder(frame).max;
|
||||
data.attributes = [];
|
||||
|
||||
if (this._createEvent) {
|
||||
this._createEvent.addValues({
|
||||
mode: this._defaultMode,
|
||||
type: this._defaultType,
|
||||
label: this._defaultLabel,
|
||||
frame,
|
||||
});
|
||||
}
|
||||
|
||||
// FIXME: In the future we have to make some generic solution
|
||||
if (this._defaultMode === 'interpolation'
|
||||
&& ['box', 'points', 'box_by_4_points'].includes(this._defaultType)) {
|
||||
data.shapes = [];
|
||||
data.shapes.push(Object.assign({}, result, data));
|
||||
this._shapeCollection.add(data, `interpolation_${this._defaultType}`);
|
||||
} else {
|
||||
Object.assign(data, result);
|
||||
this._shapeCollection.add(data, `annotation_${this._defaultType}`);
|
||||
}
|
||||
|
||||
const model = this._shapeCollection.shapes.slice(-1)[0];
|
||||
|
||||
// Undo/redo code
|
||||
window.cvat.addAction('Draw Object', () => {
|
||||
model.removed = true;
|
||||
model.unsubscribe(this._shapeCollection);
|
||||
}, () => {
|
||||
model.subscribe(this._shapeCollection);
|
||||
model.removed = false;
|
||||
}, window.cvat.player.frames.current);
|
||||
// End of undo/redo code
|
||||
|
||||
this._shapeCollection.update();
|
||||
}
|
||||
|
||||
switchCreateMode(forceClose, usingShortkey) {
|
||||
this._usingShortkey = usingShortkey;
|
||||
// if parameter force (bool) setup to true, current result will not save
|
||||
if (!forceClose) {
|
||||
this._createMode = !this._createMode && window.cvat.mode == null;
|
||||
if (this._createMode) {
|
||||
this._createEvent = Logger.addContinuedEvent(Logger.EventType.drawObject);
|
||||
window.cvat.mode = 'creation';
|
||||
} else if (window.cvat.mode === 'creation') {
|
||||
window.cvat.mode = null;
|
||||
}
|
||||
} else {
|
||||
this._createMode = false;
|
||||
if (window.cvat.mode === 'creation') {
|
||||
window.cvat.mode = null;
|
||||
if (this._createEvent) {
|
||||
this._createEvent.close();
|
||||
this._createEvent = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
this._saveCurrent = !forceClose;
|
||||
this.notify();
|
||||
}
|
||||
|
||||
get currentShapes() {
|
||||
this._shapeCollection.update();
|
||||
return this._shapeCollection.currentShapes;
|
||||
}
|
||||
|
||||
get saveCurrent() {
|
||||
return this._saveCurrent;
|
||||
}
|
||||
|
||||
get createMode() {
|
||||
return this._createMode;
|
||||
}
|
||||
|
||||
get usingShortkey() {
|
||||
return this._usingShortkey;
|
||||
}
|
||||
|
||||
get defaultType() {
|
||||
return this._defaultType;
|
||||
}
|
||||
|
||||
set defaultType(type) {
|
||||
if (!['box', 'box_by_4_points', 'points', 'polygon', 'polyline', "cuboid"].includes(type)) {
|
||||
throw Error(`Unknown shape type found ${type}`);
|
||||
}
|
||||
this._defaultType = type;
|
||||
}
|
||||
|
||||
get defaultMode() {
|
||||
return this._defaultMode;
|
||||
}
|
||||
|
||||
set defaultMode(mode) {
|
||||
this._defaultMode = mode;
|
||||
}
|
||||
|
||||
set defaultLabel(labelId) {
|
||||
this._defaultLabel = +labelId;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ShapeCreatorController {
|
||||
constructor(drawerModel) {
|
||||
this._model = drawerModel;
|
||||
setupShortkeys.call(this);
|
||||
function setupShortkeys() {
|
||||
let shortkeys = window.cvat.config.shortkeys;
|
||||
|
||||
let switchDrawHandler = Logger.shortkeyLogDecorator(function() {
|
||||
this.switchCreateMode(false, true);
|
||||
}.bind(this));
|
||||
|
||||
Mousetrap.bind(shortkeys["switch_draw_mode"].value, switchDrawHandler.bind(this), 'keydown');
|
||||
}
|
||||
}
|
||||
|
||||
switchCreateMode(force, usingShortkey = false) {
|
||||
this._model.switchCreateMode(force, usingShortkey);
|
||||
}
|
||||
|
||||
setDefaultShapeType(type) {
|
||||
this._model.defaultType = type;
|
||||
}
|
||||
|
||||
setDefaultShapeMode(mode) {
|
||||
this._model.defaultMode = mode;
|
||||
}
|
||||
|
||||
setDefaultShapeLabel(labelId) {
|
||||
this._model.defaultLabel = labelId;
|
||||
}
|
||||
|
||||
finish(result) {
|
||||
this._model.finish(result);
|
||||
}
|
||||
|
||||
get currentShapes() {
|
||||
return this._model.currentShapes;
|
||||
}
|
||||
}
|
||||
|
||||
class ShapeCreatorView {
|
||||
constructor(drawerModel, drawerController) {
|
||||
drawerModel.subscribe(this);
|
||||
this._controller = drawerController;
|
||||
this._createButton = $('#createShapeButton');
|
||||
this._labelSelector = $('#shapeLabelSelector');
|
||||
this._modeSelector = $('#shapeModeSelector');
|
||||
this._typeSelector = $('#shapeTypeSelector');
|
||||
this._polyShapeSizeInput = $('#polyShapeSize');
|
||||
this._commonBordersCheckbox = $('#commonBordersCheckbox');
|
||||
this._frameContent = SVG.adopt($('#frameContent')[0]);
|
||||
this._frameText = SVG.adopt($('#frameText')[0]);
|
||||
this._playerFrame = $('#playerFrame');
|
||||
this._createButton.on('click', () => this._controller.switchCreateMode(false));
|
||||
this._drawInstance = null;
|
||||
this._aim = null;
|
||||
this._aimCoord = {
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
this._polyShapeSize = 0;
|
||||
this._type = null;
|
||||
this._mode = null;
|
||||
this._cancel = false;
|
||||
this._scale = 1;
|
||||
this._borderSticker = null;
|
||||
|
||||
let shortkeys = window.cvat.config.shortkeys;
|
||||
this._createButton.attr('title', `
|
||||
${shortkeys['switch_draw_mode'].view_value} - ${shortkeys['switch_draw_mode'].description}`);
|
||||
|
||||
this._labelSelector.attr('title', `
|
||||
${shortkeys['change_default_label'].view_value} - ${shortkeys['change_default_label'].description}`);
|
||||
|
||||
const labels = window.cvat.labelsInfo.labels();
|
||||
const labelsKeys = Object.keys(labels);
|
||||
for (let i = 0; i < labelsKeys.length; i += 1) {
|
||||
this._labelSelector.append(
|
||||
// eslint-disable-next-line
|
||||
$(`<option value=${labelsKeys[i]}> ${labels[labelsKeys[i]].normalize()} </option>`)
|
||||
);
|
||||
}
|
||||
this._labelSelector.val(labelsKeys[0]);
|
||||
|
||||
this._typeSelector.val('box');
|
||||
this._typeSelector.on('change', (e) => {
|
||||
// FIXME: In the future we have to make some generic solution
|
||||
const mode = this._modeSelector.prop('value');
|
||||
const type = $(e.target).prop('value');
|
||||
if (type !== 'box' && type !== 'box_by_4_points'
|
||||
&& !(type === 'points' && this._polyShapeSize === 1) && mode !== 'annotation') {
|
||||
this._modeSelector.prop('value', 'annotation');
|
||||
this._controller.setDefaultShapeMode('annotation');
|
||||
showMessage('Only the annotation mode allowed for the shape');
|
||||
}
|
||||
this._controller.setDefaultShapeType(type);
|
||||
}).trigger('change');
|
||||
|
||||
this._labelSelector.on('change', (e) => {
|
||||
this._controller.setDefaultShapeLabel($(e.target).prop('value'));
|
||||
}).trigger('change');
|
||||
|
||||
this._modeSelector.on('change', (e) => {
|
||||
// FIXME: In the future we have to make some generic solution
|
||||
const mode = $(e.target).prop('value');
|
||||
const type = this._typeSelector.prop('value');
|
||||
if (mode !== 'annotation' && !(type === 'points' && this._polyShapeSize === 1)
|
||||
&& type !== 'box' && type !== 'box_by_4_points') {
|
||||
this._typeSelector.prop('value', 'box');
|
||||
this._controller.setDefaultShapeType('box');
|
||||
showMessage('Only boxes and single point allowed in the interpolation mode');
|
||||
}
|
||||
this._controller.setDefaultShapeMode(mode);
|
||||
}).trigger('change');
|
||||
|
||||
this._polyShapeSizeInput.on('change', (e) => {
|
||||
e.stopPropagation();
|
||||
let size = +e.target.value;
|
||||
if (size < 0) size = 0;
|
||||
if (size > 100) size = 0;
|
||||
const mode = this._modeSelector.prop('value');
|
||||
const type = this._typeSelector.prop('value');
|
||||
if (mode === 'interpolation' && type === 'points' && size !== 1) {
|
||||
showMessage('Only single point allowed in the interpolation mode');
|
||||
size = 1;
|
||||
}
|
||||
|
||||
e.target.value = size || '';
|
||||
this._polyShapeSize = size;
|
||||
}).trigger('change');
|
||||
|
||||
this._polyShapeSizeInput.on('keydown', function(e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
this._playerFrame.on('mousemove.shapeCreatorAIM', (e) => {
|
||||
if (!['polygon', 'polyline', 'points'].includes(this._type)) {
|
||||
this._aimCoord = window.cvat.translate.point.clientToCanvas(this._frameContent.node, e.clientX, e.clientY);
|
||||
if (this._aim) {
|
||||
this._aim.x.attr({
|
||||
y1: this._aimCoord.y,
|
||||
y2: this._aimCoord.y,
|
||||
});
|
||||
|
||||
this._aim.y.attr({
|
||||
x1: this._aimCoord.x,
|
||||
x2: this._aimCoord.x,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._commonBordersCheckbox.on('change.shapeCreator', (e) => {
|
||||
if (this._drawInstance) {
|
||||
if (!e.target.checked) {
|
||||
if (this._borderSticker) {
|
||||
this._borderSticker.disable();
|
||||
this._borderSticker = null;
|
||||
}
|
||||
} else {
|
||||
this._borderSticker = new BorderSticker(this._drawInstance, this._frameContent,
|
||||
this._controller.currentShapes, this._scale);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_createCuboidEvent() {
|
||||
let sizeUI = null;
|
||||
const backFaceOffset = 20;
|
||||
this._drawInstance = this._frameContent.rect().draw({ snapToGrid: 0.1 }).addClass("shapeCreation").attr({
|
||||
"stroke-width": STROKE_WIDTH / this._scale,
|
||||
})
|
||||
.on("drawstop", (e) => {
|
||||
if (this._cancel) {
|
||||
return;
|
||||
}
|
||||
if (sizeUI) {
|
||||
sizeUI.rm();
|
||||
sizeUI = null;
|
||||
}
|
||||
|
||||
const rect = window.cvat.translate.box.canvasToActual(e.target.getBBox());
|
||||
|
||||
const p1 = { x: rect.x, y: rect.y + 1 };
|
||||
const p2 = { x: rect.x, y: rect.y - 1 + rect.height };
|
||||
const p3 = { x: rect.x + rect.width, y: rect.y };
|
||||
const p4 = { x: rect.x + rect.width, y: rect.y + rect.height };
|
||||
|
||||
const p5 = { x: p3.x + backFaceOffset, y: p3.y - backFaceOffset + 1 };
|
||||
const p6 = { x: p3.x + backFaceOffset, y: p4.y - backFaceOffset - 1 };
|
||||
|
||||
let points = [p1, p2, p3, p4, p5, p6];
|
||||
|
||||
if (!CuboidModel.isWithinFrame(points)) {
|
||||
this._controller.switchCreateMode(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const viewModel = new Cuboid2PointViewModel(points);
|
||||
points = viewModel.getPoints();
|
||||
|
||||
points = PolyShapeModel.convertNumberArrayToString(points);
|
||||
e.target.setAttribute("points",
|
||||
window.cvat.translate.points.actualToCanvas(points));
|
||||
this._controller.finish({ points }, this._type);
|
||||
this._controller.switchCreateMode(true);
|
||||
})
|
||||
.on("drawupdate", (e) => {
|
||||
sizeUI = drawBoxSize.call(sizeUI, this._frameContent,
|
||||
this._frameText, e.target.getBBox());
|
||||
})
|
||||
.on("drawcancel", () => {
|
||||
if (sizeUI) {
|
||||
sizeUI.rm();
|
||||
sizeUI = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_createPolyEvents() {
|
||||
// If number of points for poly shape specified, use it.
|
||||
// Dicrement number on draw new point events. Drawstart trigger when create first point
|
||||
let lastPoint = {
|
||||
x: null,
|
||||
y: null,
|
||||
};
|
||||
|
||||
let numberOfPoints = 0;
|
||||
this._drawInstance.attr({
|
||||
z_order: Number.MAX_SAFE_INTEGER,
|
||||
});
|
||||
|
||||
if (this._polyShapeSize) {
|
||||
let size = this._polyShapeSize;
|
||||
const sizeDecrement = function sizeDecrement() {
|
||||
size -= 1;
|
||||
if (!size) {
|
||||
numberOfPoints = this._polyShapeSize;
|
||||
this._drawInstance.draw('done');
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
const sizeIncrement = function sizeIncrement() {
|
||||
size += 1;
|
||||
};
|
||||
|
||||
this._drawInstance.on('drawstart', sizeDecrement);
|
||||
this._drawInstance.on('drawpoint', sizeDecrement);
|
||||
this._drawInstance.on('undopoint', sizeIncrement);
|
||||
}
|
||||
// Otherwise draw will stop by Ctrl + N press
|
||||
|
||||
this._drawInstance.on('drawpoint', () => {
|
||||
if (this._borderSticker) {
|
||||
this._borderSticker.reset();
|
||||
}
|
||||
});
|
||||
|
||||
// Callbacks for point scale
|
||||
this._drawInstance.on('drawstart', this._rescaleDrawPoints.bind(this));
|
||||
this._drawInstance.on('drawpoint', this._rescaleDrawPoints.bind(this));
|
||||
|
||||
this._drawInstance.on('drawstart', (e) => {
|
||||
lastPoint = {
|
||||
x: e.detail.event.clientX,
|
||||
y: e.detail.event.clientY,
|
||||
};
|
||||
numberOfPoints ++;
|
||||
});
|
||||
|
||||
this._drawInstance.on('drawpoint', (e) => {
|
||||
lastPoint = {
|
||||
x: e.detail.event.clientX,
|
||||
y: e.detail.event.clientY,
|
||||
};
|
||||
numberOfPoints += 1;
|
||||
if (this._type === "cuboid" && numberOfPoints === 4) {
|
||||
this._drawInstance.draw("done");
|
||||
}
|
||||
});
|
||||
|
||||
this._commonBordersCheckbox.css('display', '').trigger('change.shapeCreator');
|
||||
this._commonBordersCheckbox.parent().css('display', '');
|
||||
$('body').on('keydown.shapeCreator', (e) => {
|
||||
if (e.ctrlKey && e.keyCode === 17) {
|
||||
this._commonBordersCheckbox.prop('checked', !this._borderSticker);
|
||||
this._commonBordersCheckbox.trigger('change.shapeCreator');
|
||||
}
|
||||
});
|
||||
|
||||
this._frameContent.on('mousedown.shapeCreator', (e) => {
|
||||
if (e.which === 3) {
|
||||
let lenBefore = this._drawInstance.array().value.length;
|
||||
this._drawInstance.draw('undo');
|
||||
if (this._borderSticker) {
|
||||
this._borderSticker.reset();
|
||||
}
|
||||
let lenAfter = this._drawInstance.array().value.length;
|
||||
if (lenBefore != lenAfter) {
|
||||
numberOfPoints --;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._frameContent.on('mousemove.shapeCreator', (e) => {
|
||||
if (e.shiftKey && ['polygon', 'polyline'].includes(this._type)) {
|
||||
if (lastPoint.x === null || lastPoint.y === null) {
|
||||
this._drawInstance.draw('point', e);
|
||||
}
|
||||
else {
|
||||
let delta = Math.sqrt(Math.pow(e.clientX - lastPoint.x, 2) + Math.pow(e.clientY - lastPoint.y, 2));
|
||||
let deltaTreshold = 15;
|
||||
if (delta > deltaTreshold) {
|
||||
this._drawInstance.draw('point', e);
|
||||
lastPoint = {
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._drawInstance.on('drawstop', () => {
|
||||
this._frameContent.off('mousedown.shapeCreator');
|
||||
this._frameContent.off('mousemove.shapeCreator');
|
||||
this._commonBordersCheckbox.css('display', 'none');
|
||||
this._commonBordersCheckbox.parent().css('display', 'none');
|
||||
$('body').off('keydown.shapeCreator');
|
||||
if (this._borderSticker) {
|
||||
this._borderSticker.disable();
|
||||
this._borderSticker = null;
|
||||
}
|
||||
});
|
||||
// Also we need callback on drawdone event for get points
|
||||
this._drawInstance.on("drawdone", (e) => {
|
||||
let actualPoints = window.cvat.translate.points.canvasToActual(e.target.getAttribute("points"));
|
||||
actualPoints = PolyShapeModel.convertStringToNumberArray(actualPoints);
|
||||
|
||||
// Min 2 points for polyline and 3 points for polygon
|
||||
if (actualPoints.length) {
|
||||
if (this._type === 'polyline' && actualPoints.length < 2) {
|
||||
showMessage("Min 2 points must be for polyline drawing.");
|
||||
}
|
||||
else if (this._type === 'polygon' && actualPoints.length < 3) {
|
||||
showMessage("Min 3 points must be for polygon drawing.");
|
||||
} else if (this._type === "cuboid" && (actualPoints.length !== 4)) {
|
||||
showMessage("Exactly 4 points must be used for cuboid drawing."
|
||||
+ " Second point must be below the first point."
|
||||
+ "(HINT) The first 3 points define the front face"
|
||||
+ " and the last point should define the depth and orientation of the cuboid ");
|
||||
} else if (this._type === "cuboid") {
|
||||
let points = this._makeCuboid(actualPoints);
|
||||
const viewModel = new Cuboid2PointViewModel(points);
|
||||
if (!CuboidModel.isWithinFrame(points)) {
|
||||
this._controller.switchCreateMode(true);
|
||||
return;
|
||||
}
|
||||
|
||||
points = viewModel.getPoints();
|
||||
|
||||
points = PolyShapeModel.convertNumberArrayToString(points);
|
||||
e.target.setAttribute("points",
|
||||
window.cvat.translate.points.actualToCanvas(points));
|
||||
this._controller.finish({ points }, this._type);
|
||||
this._controller.switchCreateMode(true);
|
||||
} else {
|
||||
const { frameWidth } = window.cvat.player.geometry;
|
||||
const { frameHeight } = window.cvat.player.geometry;
|
||||
for (const point of actualPoints) {
|
||||
point.x = Math.clamp(point.x, 0, frameWidth);
|
||||
point.y = Math.clamp(point.y, 0, frameHeight);
|
||||
}
|
||||
actualPoints = PolyShapeModel.convertNumberArrayToString(actualPoints);
|
||||
|
||||
// Update points in a view in order to get an updated box
|
||||
e.target.setAttribute("points", window.cvat.translate.points.actualToCanvas(actualPoints));
|
||||
const polybox = e.target.getBBox();
|
||||
const w = polybox.width;
|
||||
const h = polybox.height;
|
||||
const area = w * h;
|
||||
const type = this._type;
|
||||
|
||||
if (area >= AREA_TRESHOLD || type === "points" && numberOfPoints || type === "polyline" && (w >= AREA_TRESHOLD || h >= AREA_TRESHOLD)) {
|
||||
this._controller.finish({ points: actualPoints }, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._controller.switchCreateMode(true);
|
||||
});
|
||||
}
|
||||
|
||||
_sortClockwise(points){
|
||||
points.sort((a, b) => a.y - b.y);
|
||||
// Get center y
|
||||
const cy = (points[0].y + points[points.length - 1].y) / 2;
|
||||
|
||||
// Sort from right to left
|
||||
points.sort((a, b) => b.x - a.x);
|
||||
|
||||
// Get center x
|
||||
const cx = (points[0].x + points[points.length - 1].x) / 2;
|
||||
|
||||
// Center point
|
||||
var center = {
|
||||
x : cx,
|
||||
y : cy
|
||||
};
|
||||
|
||||
// Starting angle used to reference other angles
|
||||
var startAng;
|
||||
points.forEach((point) => {
|
||||
var ang = Math.atan2(point.y - center.y, point.x - center.x);
|
||||
if (!startAng) {
|
||||
startAng = ang;
|
||||
} else {
|
||||
if (ang < startAng) { // ensure that all points are clockwise of the start point
|
||||
ang += Math.PI * 2;
|
||||
}
|
||||
}
|
||||
point.angle = ang; // add the angle to the point
|
||||
});
|
||||
|
||||
// first sort clockwise
|
||||
points.sort((a, b) => a.angle - b.angle);
|
||||
return points.reverse();
|
||||
}
|
||||
|
||||
_makeCuboid(actualPoints){
|
||||
let unsortedPlanePoints = actualPoints.slice(0,3);
|
||||
function rotate( array , times ){
|
||||
while( times-- ){
|
||||
var temp = array.shift();
|
||||
array.push( temp );
|
||||
}
|
||||
}
|
||||
|
||||
let plane1;
|
||||
let plane2 = {p1:actualPoints[0], p2:actualPoints[0], p3:actualPoints[0], p4:actualPoints[0]};
|
||||
|
||||
// completing the plane
|
||||
const vector = {
|
||||
x: actualPoints[2].x - actualPoints[1].x,
|
||||
y: actualPoints[2].y - actualPoints[1].y,
|
||||
}
|
||||
|
||||
// sorting the first plane
|
||||
unsortedPlanePoints.push({x:actualPoints[0].x + vector.x, y: actualPoints[0].y + vector.y});
|
||||
let sortedPlanePoints = this._sortClockwise(unsortedPlanePoints);
|
||||
let leftIndex = 0;
|
||||
for(let i = 0; i<4; i++){
|
||||
leftIndex = sortedPlanePoints[`${i}`].x < sortedPlanePoints[`${leftIndex}`].x ? i : leftIndex;
|
||||
}
|
||||
rotate(sortedPlanePoints,leftIndex);
|
||||
plane1 = {
|
||||
p1:sortedPlanePoints[0],
|
||||
p2:sortedPlanePoints[1],
|
||||
p3:sortedPlanePoints[2],
|
||||
p4:sortedPlanePoints[3]
|
||||
};
|
||||
|
||||
const vec = {
|
||||
x: actualPoints[3].x - actualPoints[2].x,
|
||||
y: actualPoints[3].y - actualPoints[2].y,
|
||||
};
|
||||
// determine the orientation
|
||||
let angle = Math.atan2(vec.y,vec.x);
|
||||
|
||||
// making the other plane
|
||||
plane2.p1 = {x:plane1.p1.x + vec.x, y:plane1.p1.y + vec.y};
|
||||
plane2.p2 = {x:plane1.p2.x + vec.x, y:plane1.p2.y + vec.y};
|
||||
plane2.p3 = {x:plane1.p3.x + vec.x, y:plane1.p3.y + vec.y};
|
||||
plane2.p4 = {x:plane1.p4.x + vec.x, y:plane1.p4.y + vec.y};
|
||||
|
||||
|
||||
let points ;
|
||||
// right
|
||||
if(Math.abs(angle) < Math.PI/2-0.1){
|
||||
return this._setupCuboidPoints(actualPoints);
|
||||
}
|
||||
|
||||
// left
|
||||
else if(Math.abs(angle) > Math.PI/2+0.1){
|
||||
return this._setupCuboidPoints(actualPoints);
|
||||
}
|
||||
// down
|
||||
else if(angle>0){
|
||||
points = [plane1.p1,plane2.p1,plane1.p2,plane2.p2,plane1.p3,plane2.p3];
|
||||
points[0].y+=0.1;
|
||||
points[4].y+=0.1;
|
||||
return [plane1.p1,plane2.p1,plane1.p2,plane2.p2,plane1.p3,plane2.p3];
|
||||
}
|
||||
// up
|
||||
else{
|
||||
points = [plane2.p1,plane1.p1,plane2.p2,plane1.p2,plane2.p3,plane1.p3];
|
||||
points[0].y+=0.1;
|
||||
points[4].y+=0.1;
|
||||
return points;
|
||||
}
|
||||
}
|
||||
|
||||
_setupCuboidPoints(actualPoints) {
|
||||
let left,right,left2,right2;
|
||||
let p1,p2,p3,p4,p5,p6;
|
||||
|
||||
const height = Math.abs(actualPoints[0].x - actualPoints[1].x)
|
||||
< Math.abs(actualPoints[1].x - actualPoints[2].x)
|
||||
? Math.abs(actualPoints[1].y - actualPoints[0].y)
|
||||
: Math.abs(actualPoints[1].y - actualPoints[2].y);
|
||||
|
||||
// seperate into left and right point
|
||||
// we pick the first and third point because we know assume they will be on
|
||||
// opposite corners
|
||||
if(actualPoints[0].x < actualPoints[2].x){
|
||||
left = actualPoints[0];
|
||||
right = actualPoints[2];
|
||||
}else{
|
||||
left = actualPoints[2];
|
||||
right = actualPoints[0];
|
||||
}
|
||||
|
||||
// get other 2 points using the given height
|
||||
if(left.y < right.y){
|
||||
left2 = { x: left.x, y: left.y + height };
|
||||
right2 = { x: right.x, y: right.y - height };
|
||||
}else{
|
||||
left2 = { x: left.x, y: left.y - height };
|
||||
right2 = { x: right.x, y: right.y + height };
|
||||
}
|
||||
|
||||
// get the vector for the last point relative to the previous point
|
||||
const vec = {
|
||||
x: actualPoints[3].x - actualPoints[2].x,
|
||||
y: actualPoints[3].y - actualPoints[2].y,
|
||||
};
|
||||
|
||||
if(left.y < left2.y){
|
||||
p1 = left;
|
||||
p2 = left2;
|
||||
}else{
|
||||
p1 = left2;
|
||||
p2 = left;
|
||||
}
|
||||
|
||||
if(right.y < right2.y){
|
||||
p3 = right;
|
||||
p4 = right2;
|
||||
}else{
|
||||
p3 = right2;
|
||||
p4 = right;
|
||||
}
|
||||
|
||||
p5 = { x: p3.x + vec.x, y: p3.y + vec.y + 0.1 };
|
||||
p6 = { x: p4.x + vec.x, y: p4.y + vec.y - 0.1 };
|
||||
|
||||
p1.y += 0.1;
|
||||
return [p1, p2, p3, p4, p5, p6];
|
||||
}
|
||||
|
||||
_create() {
|
||||
let sizeUI = null;
|
||||
switch(this._type) {
|
||||
case 'box':
|
||||
this._drawInstance = this._frameContent.rect().draw({ snapToGrid: 0.1 }).addClass('shapeCreation').attr({
|
||||
'stroke-width': STROKE_WIDTH / this._scale,
|
||||
z_order: Number.MAX_SAFE_INTEGER,
|
||||
}).on('drawstop', function(e) {
|
||||
if (this._cancel) return;
|
||||
if (sizeUI) {
|
||||
sizeUI.rm();
|
||||
sizeUI = null;
|
||||
}
|
||||
|
||||
const frameWidth = window.cvat.player.geometry.frameWidth;
|
||||
const frameHeight = window.cvat.player.geometry.frameHeight;
|
||||
const rect = window.cvat.translate.box.canvasToActual(e.target.getBBox());
|
||||
|
||||
const xtl = Math.clamp(rect.x, 0, frameWidth);
|
||||
const ytl = Math.clamp(rect.y, 0, frameHeight);
|
||||
const xbr = Math.clamp(rect.x + rect.width, 0, frameWidth);
|
||||
const ybr = Math.clamp(rect.y + rect.height, 0, frameHeight);
|
||||
if ((ybr - ytl) * (xbr - xtl) >= AREA_TRESHOLD) {
|
||||
const box = {
|
||||
xtl,
|
||||
ytl,
|
||||
xbr,
|
||||
ybr,
|
||||
};
|
||||
|
||||
if (this._mode === 'interpolation') {
|
||||
box.outside = false;
|
||||
}
|
||||
|
||||
this._controller.finish(box, this._type);
|
||||
}
|
||||
|
||||
this._controller.switchCreateMode(true);
|
||||
}.bind(this)).on('drawupdate', (e) => {
|
||||
sizeUI = drawBoxSize.call(sizeUI, this._frameContent, this._frameText, e.target.getBBox());
|
||||
}).on('drawcancel', () => {
|
||||
if (sizeUI) {
|
||||
sizeUI.rm();
|
||||
sizeUI = null;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'box_by_4_points':
|
||||
let numberOfPoints = 0;
|
||||
this._drawInstance = this._frameContent.polyline().draw({ snapToGrid: 0.1 })
|
||||
.addClass('shapeCreation').attr({
|
||||
'stroke-width': 0,
|
||||
}).on('drawstart', () => {
|
||||
// init numberOfPoints as one on drawstart
|
||||
numberOfPoints = 1;
|
||||
}).on('drawpoint', (e) => {
|
||||
// increase numberOfPoints by one on drawpoint
|
||||
numberOfPoints += 1;
|
||||
|
||||
// finish if numberOfPoints are exactly four
|
||||
if (numberOfPoints === 4) {
|
||||
let actualPoints = window.cvat.translate.points.canvasToActual(e.target.getAttribute('points'));
|
||||
actualPoints = PolyShapeModel.convertStringToNumberArray(actualPoints);
|
||||
const { frameWidth, frameHeight } = window.cvat.player.geometry;
|
||||
|
||||
// init bounding box
|
||||
const box = {
|
||||
'xtl': frameWidth,
|
||||
'ytl': frameHeight,
|
||||
'xbr': 0,
|
||||
'ybr': 0
|
||||
};
|
||||
|
||||
for (const point of actualPoints) {
|
||||
// clamp point
|
||||
point.x = Math.clamp(point.x, 0, frameWidth);
|
||||
point.y = Math.clamp(point.y, 0, frameHeight);
|
||||
|
||||
// update bounding box
|
||||
box.xtl = Math.min(point.x, box.xtl);
|
||||
box.ytl = Math.min(point.y, box.ytl);
|
||||
box.xbr = Math.max(point.x, box.xbr);
|
||||
box.ybr = Math.max(point.y, box.ybr);
|
||||
}
|
||||
|
||||
if ((box.ybr - box.ytl) * (box.xbr - box.xtl) >= AREA_TRESHOLD) {
|
||||
if (this._mode === 'interpolation') {
|
||||
box.outside = false;
|
||||
}
|
||||
// finish drawing
|
||||
this._controller.finish(box, this._type);
|
||||
}
|
||||
this._controller.switchCreateMode(true);
|
||||
}
|
||||
}).on('undopoint', () => {
|
||||
if (numberOfPoints > 0) {
|
||||
numberOfPoints -= 1;
|
||||
}
|
||||
}).off('drawdone').on('drawdone', () => {
|
||||
if (numberOfPoints !== 4) {
|
||||
showMessage('Click exactly four extreme points for an object');
|
||||
this._controller.switchCreateMode(true);
|
||||
} else {
|
||||
throw Error('numberOfPoints is exactly four, but box drawing did not finish.');
|
||||
}
|
||||
});
|
||||
this._createPolyEvents();
|
||||
break;
|
||||
case 'points':
|
||||
this._drawInstance = this._frameContent.polyline().draw({ snapToGrid: 0.1 })
|
||||
.addClass('shapeCreation').attr({
|
||||
'stroke-width': 0,
|
||||
});
|
||||
this._createPolyEvents();
|
||||
break;
|
||||
case 'polygon':
|
||||
if (this._polyShapeSize && this._polyShapeSize < 3) {
|
||||
if (!$('.drawAllert').length) {
|
||||
showMessage("Min 3 points must be for polygon drawing.").addClass('drawAllert');
|
||||
}
|
||||
this._controller.switchCreateMode(true);
|
||||
return;
|
||||
}
|
||||
this._drawInstance = this._frameContent.polygon().draw({ snapToGrid: 0.1 })
|
||||
.addClass('shapeCreation').attr({
|
||||
'stroke-width': STROKE_WIDTH / this._scale,
|
||||
});
|
||||
this._createPolyEvents();
|
||||
break;
|
||||
case 'polyline':
|
||||
if (this._polyShapeSize && this._polyShapeSize < 2) {
|
||||
if (!$('.drawAllert').length) {
|
||||
showMessage("Min 2 points must be for polyline drawing.").addClass('drawAllert');
|
||||
}
|
||||
this._controller.switchCreateMode(true);
|
||||
return;
|
||||
}
|
||||
this._drawInstance = this._frameContent.polyline().draw({ snapToGrid: 0.1 })
|
||||
.addClass('shapeCreation').attr({
|
||||
'stroke-width': STROKE_WIDTH / this._scale,
|
||||
});
|
||||
this._createPolyEvents();
|
||||
break;
|
||||
case "cuboid":
|
||||
this._drawInstance = this._frameContent.polyline().draw({ snapToGrid: 0.1 }).addClass("shapeCreation").attr({
|
||||
"stroke-width": STROKE_WIDTH / this._scale,
|
||||
});
|
||||
this._createPolyEvents();
|
||||
break;
|
||||
default:
|
||||
throw Error(`Bad type found ${this._type}`);
|
||||
}
|
||||
}
|
||||
|
||||
_rescaleDrawPoints() {
|
||||
const scale = this._scale;
|
||||
$(".svg_draw_point").each(function () {
|
||||
this.instance.radius(2.5 / scale).attr("stroke-width", 1 / scale);
|
||||
});
|
||||
}
|
||||
|
||||
_drawAim() {
|
||||
if (!(this._aim)) {
|
||||
this._aim = {
|
||||
x: this._frameContent.line(0, this._aimCoord.y, this._frameContent.node.clientWidth, this._aimCoord.y)
|
||||
.attr({
|
||||
'stroke-width': STROKE_WIDTH / this._scale,
|
||||
'stroke': 'red',
|
||||
'z_order': Number.MAX_SAFE_INTEGER,
|
||||
}).addClass('aim'),
|
||||
y: this._frameContent.line(this._aimCoord.x, 0, this._aimCoord.x, this._frameContent.node.clientHeight)
|
||||
.attr({
|
||||
'stroke-width': STROKE_WIDTH / this._scale,
|
||||
'stroke': 'red',
|
||||
'z_order': Number.MAX_SAFE_INTEGER,
|
||||
}).addClass('aim'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
_removeAim() {
|
||||
if (this._aim) {
|
||||
this._aim.x.remove();
|
||||
this._aim.y.remove();
|
||||
this._aim = null;
|
||||
}
|
||||
}
|
||||
|
||||
onShapeCreatorUpdate(model) {
|
||||
if (model.createMode && !this._drawInstance) {
|
||||
this._cancel = false;
|
||||
this._type = model.defaultType;
|
||||
this._mode = model.defaultMode;
|
||||
|
||||
if (!['polygon', 'polyline', 'points'].includes(this._type)) {
|
||||
if (!model.usingShortkey) {
|
||||
this._aimCoord = {
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
}
|
||||
this._drawAim();
|
||||
}
|
||||
|
||||
this._createButton.text("Stop Creation");
|
||||
document.oncontextmenu = () => false;
|
||||
this._create();
|
||||
}
|
||||
else {
|
||||
this._removeAim();
|
||||
this._cancel = true;
|
||||
this._createButton.text("Create Shape");
|
||||
document.oncontextmenu = null;
|
||||
if (this._drawInstance) {
|
||||
// We save current result for poly shape if it's need
|
||||
// drawInstance will be removed after save when drawdone handler calls switchCreateMode with force argument
|
||||
if (model.saveCurrent && this._type != 'box') {
|
||||
this._drawInstance.draw('done');
|
||||
}
|
||||
else {
|
||||
this._drawInstance.draw('cancel');
|
||||
this._drawInstance.remove();
|
||||
this._drawInstance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._typeSelector.prop('disabled', model.createMode);
|
||||
this._modeSelector.prop('disabled', model.createMode);
|
||||
this._polyShapeSizeInput.prop('disabled', model.createMode);
|
||||
}
|
||||
|
||||
onPlayerUpdate(player) {
|
||||
if (!player.ready()) return;
|
||||
if (this._scale != player.geometry.scale) {
|
||||
this._scale = player.geometry.scale;
|
||||
if (this._drawInstance) {
|
||||
this._rescaleDrawPoints();
|
||||
if (this._borderSticker) {
|
||||
this._borderSticker.scale(this._scale);
|
||||
}
|
||||
if (this._aim) {
|
||||
this._aim.x.attr('stroke-width', STROKE_WIDTH / this._scale);
|
||||
this._aim.y.attr('stroke-width', STROKE_WIDTH / this._scale);
|
||||
}
|
||||
if (['box', 'polygon', 'polyline'].includes(this._type)) {
|
||||
this._drawInstance.attr('stroke-width', STROKE_WIDTH / this._scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,246 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* exported FilterModel FilterController FilterView */
|
||||
/* eslint no-unused-vars: ["error", { "caughtErrors": "none" }] */
|
||||
|
||||
/* global
|
||||
defiant:false
|
||||
*/
|
||||
|
||||
class FilterModel {
|
||||
constructor(update) {
|
||||
this._regex = /^[0-9]+|[-,?!()\s]+/g;
|
||||
this._filter = '';
|
||||
this._update = update;
|
||||
this._labels = window.cvat.labelsInfo.labels();
|
||||
this._attributes = window.cvat.labelsInfo.attributes();
|
||||
}
|
||||
|
||||
_convertShape(shape) {
|
||||
// We replace all special characters due to defiant.js can't work with them
|
||||
function convertAttributes(attributes) {
|
||||
const convertedAttributes = {};
|
||||
for (const attrId in attributes) {
|
||||
if (Object.prototype.hasOwnProperty.call(attributes, attrId)) {
|
||||
const key = attributes[attrId].name
|
||||
.toLowerCase().replace(this._regex, '_');
|
||||
convertedAttributes[key] = String(attributes[attrId].value)
|
||||
.toLowerCase();
|
||||
}
|
||||
}
|
||||
return convertedAttributes;
|
||||
}
|
||||
|
||||
const converted = {
|
||||
id: shape.model.id,
|
||||
serverid: shape.model.serverID,
|
||||
label: shape.model.label,
|
||||
type: shape.model.type.split('_')[1],
|
||||
mode: shape.model.type.split('_')[0],
|
||||
occluded: Boolean(shape.interpolation.position.occluded),
|
||||
attr: convertAttributes.call(this, shape.interpolation.attributes),
|
||||
lock: shape.model.lock,
|
||||
};
|
||||
|
||||
if (shape.model.type.split('_')[1] === 'box') {
|
||||
converted.width = shape.interpolation.position.xbr - shape.interpolation.position.xtl;
|
||||
converted.height = shape.interpolation.position.ybr - shape.interpolation.position.ytl;
|
||||
} else {
|
||||
converted.width = shape.interpolation.position.width;
|
||||
converted.height = shape.interpolation.position.height;
|
||||
}
|
||||
|
||||
return converted;
|
||||
}
|
||||
|
||||
_convertCollection(collection) {
|
||||
const converted = {};
|
||||
for (const labelId in this._labels) {
|
||||
if (Object.prototype.hasOwnProperty.call(this._labels, labelId)) {
|
||||
converted[this._labels[labelId].toLowerCase().replace(this._regex, '_')] = [];
|
||||
}
|
||||
}
|
||||
|
||||
for (const shape of collection) {
|
||||
converted[this._labels[shape.model.label]
|
||||
.toLowerCase().replace(this._regex, '_')]
|
||||
.push(this._convertShape.call(this, shape));
|
||||
}
|
||||
return converted;
|
||||
}
|
||||
|
||||
filter(interpolation) {
|
||||
if (this._filter.length) {
|
||||
// Get shape indexes
|
||||
try {
|
||||
const idxs = defiant.search(this._convertCollection(interpolation), `(${this._filter})/id`);
|
||||
return interpolation.filter(x => idxs.indexOf(x.model.id) !== -1);
|
||||
} catch (ignore) {
|
||||
return [];
|
||||
}
|
||||
} else {
|
||||
return interpolation;
|
||||
}
|
||||
}
|
||||
|
||||
updateFilter(value, silent) {
|
||||
this._filter = value;
|
||||
if (!silent) {
|
||||
this._update();
|
||||
}
|
||||
}
|
||||
|
||||
get regex() {
|
||||
return this._regex;
|
||||
}
|
||||
}
|
||||
|
||||
class FilterController {
|
||||
constructor(filterModel) {
|
||||
this._model = filterModel;
|
||||
}
|
||||
|
||||
updateFilter(value, silent) {
|
||||
if (!value.length) {
|
||||
this._model.updateFilter('', silent);
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
value = value.toLowerCase();
|
||||
|
||||
const labels = String.customSplit(value, '[|]').map(el => el.trim());
|
||||
let result = '';
|
||||
for (const label of labels) {
|
||||
const labelName = label.match(/^[-,?!_0-9a-z()*\s"]+/)[0];
|
||||
const labelFilters = label.substr(labelName.length).trim();
|
||||
|
||||
result += `${labelName.replace(this._model.regex, '_').replace(/"/g, '')}`;
|
||||
|
||||
const orExpressions = String.customSplit(labelFilters, 'or').map(el => el.trim());
|
||||
const formattedOrExpressions = [];
|
||||
for (const orExpression of orExpressions) {
|
||||
const andExpressions = String.customSplit(orExpression, 'and').map(el => el.trim());
|
||||
const formattedAndExpressions = [];
|
||||
for (const andExpression of andExpressions) {
|
||||
if (andExpression.includes('attr/')) {
|
||||
const attrMatch = andExpression.match(/[\\[(]*attr\//);
|
||||
const attrPrefix = attrMatch[0];
|
||||
const attrExpression = andExpression.substr(attrMatch.index
|
||||
+ attrPrefix.length);
|
||||
const [attrName, attrValue] = String
|
||||
.customSplit(attrExpression, '=|<=|>=|<|>|!=');
|
||||
const condition = attrExpression
|
||||
.slice(attrName.length, -attrValue.length).trim();
|
||||
|
||||
formattedAndExpressions
|
||||
.push(`${attrPrefix}${attrName.trim().replace(this._model.regex, '_')
|
||||
.replace(/"/g, '')}${condition}${attrValue.trim()}`);
|
||||
} else {
|
||||
formattedAndExpressions.push(andExpression);
|
||||
}
|
||||
}
|
||||
|
||||
if (formattedAndExpressions.length > 1) {
|
||||
formattedOrExpressions.push(formattedAndExpressions.join(' and '));
|
||||
} else {
|
||||
formattedOrExpressions.push(formattedAndExpressions[0]);
|
||||
}
|
||||
}
|
||||
|
||||
if (formattedOrExpressions.length > 1) {
|
||||
result += `${formattedOrExpressions.join(' or ')}`;
|
||||
} else {
|
||||
result += `${formattedOrExpressions[0]}`;
|
||||
}
|
||||
|
||||
result += '|';
|
||||
}
|
||||
|
||||
result = result.substr(0, result.length - 1);
|
||||
result = result.split('|').map(x => `/d:data/${x}`).join('|');
|
||||
|
||||
document.evaluate(result, document, () => 'ns');
|
||||
|
||||
this._model.updateFilter(result, silent);
|
||||
} catch (ignore) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
deactivate() {
|
||||
this._model.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class FilterView {
|
||||
constructor(filterController) {
|
||||
this._controller = filterController;
|
||||
this._filterString = $("#filterInputString");
|
||||
this._resetFilterButton = $("#resetFilterButton");
|
||||
this._filterString.on("keypress keydown keyup", (e) => e.stopPropagation());
|
||||
this._filterSubmitList = $("#filterSubmitList");
|
||||
|
||||
let predefinedValues = null;
|
||||
try {
|
||||
predefinedValues = JSON.parse(localStorage.getItem("filterValues")) || [];
|
||||
}
|
||||
catch (ignore) {
|
||||
predefinedValues = [];
|
||||
}
|
||||
|
||||
let initSubmitList = () => {
|
||||
this._filterSubmitList.empty();
|
||||
for (let value of predefinedValues) {
|
||||
value = value.replace(/'/g, '"');
|
||||
this._filterSubmitList.append(`<option value='${value}'> ${value} </option>`);
|
||||
}
|
||||
}
|
||||
initSubmitList();
|
||||
|
||||
this._filterString.on("change", (e) => {
|
||||
let value = $.trim(e.target.value).replace(/'/g, '"');
|
||||
if (this._controller.updateFilter(value, false)) {
|
||||
this._filterString.css("color", "green");
|
||||
if (!predefinedValues.includes(value)) {
|
||||
predefinedValues = (predefinedValues.concat([value])).slice(-10);
|
||||
localStorage.setItem("filterValues", JSON.stringify(predefinedValues));
|
||||
initSubmitList();
|
||||
}
|
||||
}
|
||||
else {
|
||||
this._filterString.css("color", "red");
|
||||
this._controller.updateFilter("", false);
|
||||
}
|
||||
});
|
||||
|
||||
let shortkeys = window.cvat.config.shortkeys;
|
||||
this._filterString.attr("title", `
|
||||
${shortkeys["prev_filter_frame"].view_value} - ${shortkeys["prev_filter_frame"].description}` + `\n` +
|
||||
`${shortkeys["next_filter_frame"].view_value} - ${shortkeys["next_filter_frame"].description}`);
|
||||
|
||||
this._resetFilterButton.on("click", () => {
|
||||
this._filterString.prop("value", "");
|
||||
this._controller.updateFilter("", false);
|
||||
});
|
||||
|
||||
let initialFilter = window.cvat.search.get("filter");
|
||||
if (initialFilter) {
|
||||
this._filterString.prop("value", initialFilter);
|
||||
if (this._controller.updateFilter(initialFilter, true)) {
|
||||
this._filterString.css("color", "green");
|
||||
}
|
||||
else {
|
||||
this._filterString.prop("value", "");
|
||||
this._filterString.css("color", "red");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,274 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* exported ShapeGrouperModel ShapeGrouperController ShapeGrouperView*/
|
||||
|
||||
/* global
|
||||
Listener:false
|
||||
Logger:false
|
||||
Mousetrap:false
|
||||
STROKE_WIDTH:false
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
class ShapeGrouperModel extends Listener {
|
||||
constructor(shapeCollection) {
|
||||
super('onGrouperUpdate', () => this);
|
||||
|
||||
this._shapeCollection = shapeCollection;
|
||||
this._active = false;
|
||||
this._selectedObjects = [];
|
||||
}
|
||||
|
||||
_unselectObjects() {
|
||||
for (let obj of this._selectedObjects) {
|
||||
obj.groupping = false;
|
||||
}
|
||||
this._selectedObjects = [];
|
||||
}
|
||||
|
||||
apply() {
|
||||
if (this._selectedObjects.length) {
|
||||
this._shapeCollection.joinToGroup(this._selectedObjects);
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
if (this._selectedObjects.length) {
|
||||
this._shapeCollection.resetGroupFor(this._selectedObjects);
|
||||
}
|
||||
}
|
||||
|
||||
cancel() {
|
||||
if (this._active) {
|
||||
this._unselectObjects();
|
||||
|
||||
this._active = false;
|
||||
if (window.cvat.mode === 'groupping') {
|
||||
window.cvat.mode = null;
|
||||
}
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
|
||||
switch() {
|
||||
if (this._active) {
|
||||
this.apply();
|
||||
this.cancel();
|
||||
}
|
||||
else if (window.cvat.mode === null) {
|
||||
window.cvat.mode = 'groupping';
|
||||
this._active = true;
|
||||
this._shapeCollection.resetActive();
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
|
||||
add(model) {
|
||||
let idx = this._selectedObjects.indexOf(model);
|
||||
if (idx === -1) {
|
||||
this._selectedObjects.push(model);
|
||||
model.groupping = true;
|
||||
}
|
||||
}
|
||||
|
||||
click() {
|
||||
if (this._active) {
|
||||
let active = this._shapeCollection.selectShape(this._shapeCollection.lastPosition, true);
|
||||
if (active) {
|
||||
let idx = this._selectedObjects.indexOf(active);
|
||||
if (idx != -1) {
|
||||
this._selectedObjects.splice(idx, 1);
|
||||
active.groupping = false;
|
||||
}
|
||||
else {
|
||||
this._selectedObjects.push(active);
|
||||
active.groupping = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onCollectionUpdate() {
|
||||
if (this._active) {
|
||||
this._unselectObjects();
|
||||
}
|
||||
}
|
||||
|
||||
get active() {
|
||||
return this._active;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ShapeGrouperController {
|
||||
constructor(grouperModel) {
|
||||
this._model = grouperModel;
|
||||
|
||||
setupGrouperShortkeys.call(this);
|
||||
function setupGrouperShortkeys() {
|
||||
let shortkeys = window.cvat.config.shortkeys;
|
||||
|
||||
let switchGrouperHandler = Logger.shortkeyLogDecorator(function() {
|
||||
this.switch();
|
||||
}.bind(this));
|
||||
|
||||
let resetGroupHandler = Logger.shortkeyLogDecorator(function() {
|
||||
if (this._model.active) {
|
||||
this._model.reset();
|
||||
this._model.cancel();
|
||||
this._model.notify();
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
Mousetrap.bind(shortkeys["switch_group_mode"].value, switchGrouperHandler.bind(this), 'keydown');
|
||||
Mousetrap.bind(shortkeys["reset_group"].value, resetGroupHandler.bind(this), 'keydown');
|
||||
}
|
||||
}
|
||||
|
||||
switch() {
|
||||
this._model.switch();
|
||||
}
|
||||
|
||||
add(model) {
|
||||
this._model.add(model);
|
||||
}
|
||||
|
||||
click() {
|
||||
this._model.click();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ShapeGrouperView {
|
||||
constructor(grouperModel, grouperController) {
|
||||
this._controller = grouperController;
|
||||
this._frameContent = $('#frameContent');
|
||||
this._groupShapesButton = $('#groupShapesButton');
|
||||
this._rectSelector = null;
|
||||
this._initPoint = null;
|
||||
this._scale = 1;
|
||||
|
||||
this._groupShapesButton.on('click', () => {
|
||||
this._controller.switch();
|
||||
});
|
||||
|
||||
let shortkeys = window.cvat.config.shortkeys;
|
||||
|
||||
this._groupShapesButton.attr('title', `
|
||||
${shortkeys['switch_group_mode'].view_value} - ${shortkeys['switch_group_mode'].description}` + `\n` +
|
||||
`${shortkeys['reset_group'].view_value} - ${shortkeys['reset_group'].description}`);
|
||||
|
||||
grouperModel.subscribe(this);
|
||||
}
|
||||
|
||||
_select() {
|
||||
if (this._rectSelector) {
|
||||
let rect1 = this._rectSelector[0].getBBox();
|
||||
for (let shape of this._frameContent.find('.shape')) {
|
||||
let rect2 = shape.getBBox();
|
||||
|
||||
if (rect1.x < rect2.x && rect1.y < rect2.y &&
|
||||
rect1.x + rect1.width > rect2.x + rect2.width &&
|
||||
rect1.y + rect1.height > rect2.y + rect2.height) {
|
||||
this._controller.add(shape.cvatView.controller().model());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_reset() {
|
||||
if (this._rectSelector) {
|
||||
this._rectSelector.remove();
|
||||
this._rectSelector = null;
|
||||
}
|
||||
|
||||
if (this._initPoint) {
|
||||
this._initPoint = null;
|
||||
}
|
||||
}
|
||||
|
||||
_enableEvents() {
|
||||
this._frameContent.on('mousedown.grouper', (e) => {
|
||||
this._initPoint = window.cvat.translate.point.clientToCanvas(this._frameContent[0], e.clientX, e.clientY);
|
||||
});
|
||||
|
||||
this._frameContent.on('mousemove.grouper', (e) => {
|
||||
let currentPoint = window.cvat.translate.point.clientToCanvas(this._frameContent[0], e.clientX, e.clientY);
|
||||
|
||||
if (this._initPoint) {
|
||||
if (!this._rectSelector) {
|
||||
this._rectSelector = $(document.createElementNS('http://www.w3.org/2000/svg', 'rect'));
|
||||
this._rectSelector.appendTo(this._frameContent);
|
||||
this._rectSelector.attr({
|
||||
'stroke-width': (STROKE_WIDTH / 3) / this._scale,
|
||||
'stroke': 'darkmagenta',
|
||||
'fill': 'darkmagenta',
|
||||
'fill-opacity': 0.5,
|
||||
'stroke-dasharray': 5,
|
||||
});
|
||||
}
|
||||
|
||||
this._rectSelector.attr({
|
||||
'x': Math.min(this._initPoint.x, currentPoint.x),
|
||||
'y': Math.min(this._initPoint.y, currentPoint.y),
|
||||
'width': Math.max(this._initPoint.x, currentPoint.x) - Math.min(this._initPoint.x, currentPoint.x),
|
||||
'height': Math.max(this._initPoint.y, currentPoint.y) - Math.min(this._initPoint.y, currentPoint.y),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this._frameContent.on('mouseup.grouper', () => {
|
||||
this._select();
|
||||
this._reset();
|
||||
});
|
||||
|
||||
this._frameContent.on('mouseleave.grouper', () => {
|
||||
this._select();
|
||||
this._reset();
|
||||
});
|
||||
|
||||
this._frameContent.on('click.grouper', () => {
|
||||
this._controller.click();
|
||||
});
|
||||
}
|
||||
|
||||
_disableEvents() {
|
||||
this._frameContent.off('mousedown.grouper');
|
||||
this._frameContent.off('mousemove.grouper');
|
||||
this._frameContent.off('mouseup.grouper');
|
||||
this._frameContent.off('mouseleave.grouper');
|
||||
this._frameContent.off('click.grouper');
|
||||
}
|
||||
|
||||
onGrouperUpdate(grouper) {
|
||||
if (grouper.active) {
|
||||
this._enableEvents();
|
||||
this._groupShapesButton.text('Apply Group');
|
||||
}
|
||||
else {
|
||||
this._reset();
|
||||
this._disableEvents();
|
||||
this._groupShapesButton.text('Group Shapes');
|
||||
if (this._rectSelector) {
|
||||
this._rectSelector.remove();
|
||||
this._rectSelector = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPlayerUpdate(player) {
|
||||
if (this._scale != player.geometry.scale) {
|
||||
this._scale = player.geometry.scale;
|
||||
if (this._rectSelector) {
|
||||
this._rectSelector.attr({
|
||||
'stroke-width': STROKE_WIDTH / this._scale,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,285 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018-2020 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* exported ShapeMergerModel ShapeMergerController ShapeMergerView*/
|
||||
|
||||
/* global
|
||||
Listener:false
|
||||
Logger:false
|
||||
Mousetrap:false
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
class ShapeMergerModel extends Listener {
|
||||
constructor(collectionModel) {
|
||||
super('onShapeMergerUpdate', () => this);
|
||||
this._collectionModel = collectionModel;
|
||||
this._shapesForMerge = [];
|
||||
this._mergeMode = false;
|
||||
this._shapeType = null;
|
||||
}
|
||||
|
||||
_pushForMerge(shape) {
|
||||
if (!this._shapesForMerge.length) {
|
||||
this._shapeType = shape.type.split('_')[1];
|
||||
this._shapesForMerge.push(shape);
|
||||
shape.merge = true;
|
||||
}
|
||||
else if (shape.type.split('_')[1] == this._shapeType) {
|
||||
let idx = this._shapesForMerge.indexOf(shape);
|
||||
if (idx != -1) {
|
||||
this._shapesForMerge.splice(idx, 1);
|
||||
shape.merge = false;
|
||||
}
|
||||
else {
|
||||
this._shapesForMerge.push(shape);
|
||||
shape.merge = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
if (!window.cvat.mode) {
|
||||
window.cvat.mode = 'merge';
|
||||
this._mergeMode = true;
|
||||
this._collectionModel.resetActive();
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
|
||||
done() {
|
||||
if (this._shapesForMerge.length > 1) {
|
||||
let shapeDict = {};
|
||||
|
||||
for (let shape of this._shapesForMerge) {
|
||||
let keyframes = shape.keyframes;
|
||||
for (let keyframe of keyframes) {
|
||||
keyframe = +keyframe;
|
||||
let interpolation = shape.interpolate(keyframe);
|
||||
if (keyframe in shapeDict && !interpolation.position.outside) {
|
||||
shapeDict[keyframe] = {
|
||||
shape: shape,
|
||||
interpolation: interpolation
|
||||
};
|
||||
}
|
||||
else if (!(keyframe in shapeDict)) {
|
||||
shapeDict[keyframe] = {
|
||||
shape: shape,
|
||||
interpolation: interpolation
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let sortedFrames = Object.keys(shapeDict).map(x => +x).sort((a,b) => a - b);
|
||||
|
||||
// remove all outside in begin of the track
|
||||
while (shapeDict[sortedFrames[0]].interpolation.position.outside) {
|
||||
delete shapeDict[sortedFrames[0]];
|
||||
sortedFrames.splice(0, 1);
|
||||
}
|
||||
|
||||
// if several shapes placed on single frame, do not merge
|
||||
if (Object.keys(shapeDict).length <= 1) {
|
||||
this.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
let label = shapeDict[sortedFrames[0]].shape.label;
|
||||
|
||||
let object = {
|
||||
label_id: label,
|
||||
group: 0,
|
||||
frame: sortedFrames[0],
|
||||
attributes: [],
|
||||
shapes: [],
|
||||
};
|
||||
|
||||
let lastMutableAttr = {};
|
||||
let attributes = shapeDict[sortedFrames[0]].interpolation.attributes;
|
||||
for (let attrId in attributes) {
|
||||
if (!window.cvat.labelsInfo.attrInfo(attrId).mutable) {
|
||||
object.attributes.push({
|
||||
id: attrId,
|
||||
value: attributes[attrId].value,
|
||||
});
|
||||
}
|
||||
else {
|
||||
lastMutableAttr[attrId] = null;
|
||||
}
|
||||
}
|
||||
|
||||
for (let frame of sortedFrames) {
|
||||
|
||||
// Not save continiously attributes. Only updates.
|
||||
let shapeAttributes = [];
|
||||
if (shapeDict[frame].shape.label === label) {
|
||||
let attributes = shapeDict[frame].interpolation.attributes;
|
||||
for (let attrId in attributes) {
|
||||
if (window.cvat.labelsInfo.attrInfo(attrId).mutable) {
|
||||
if (attributes[attrId].value != lastMutableAttr[attrId]) {
|
||||
lastMutableAttr[attrId] = attributes[attrId].value;
|
||||
shapeAttributes.push({
|
||||
id: attrId,
|
||||
value: attributes[attrId].value,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object.shapes.push(
|
||||
Object.assign(shapeDict[frame].interpolation.position,
|
||||
{
|
||||
frame: frame,
|
||||
attributes: shapeAttributes
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// push an outsided box after each annotation box if next frame is empty
|
||||
let nextFrame = frame + 1;
|
||||
let stopFrame = window.cvat.player.frames.stop;
|
||||
let type = shapeDict[frame].shape.type;
|
||||
if (type.startsWith('annotation_') && !(nextFrame in shapeDict) && nextFrame <= stopFrame) {
|
||||
let copy = Object.assign({}, object.shapes[object.shapes.length - 1]);
|
||||
copy.outside = true;
|
||||
copy.frame += 1;
|
||||
copy.z_order = 0;
|
||||
copy.attributes = [];
|
||||
object.shapes.push(copy);
|
||||
}
|
||||
}
|
||||
|
||||
Logger.addEvent(Logger.EventType.mergeObjects, {
|
||||
count: this._shapesForMerge.length,
|
||||
});
|
||||
|
||||
this._collectionModel.add(object, `interpolation_${this._shapeType}`);
|
||||
this._collectionModel.update();
|
||||
|
||||
let model = this._collectionModel.shapes.slice(-1)[0];
|
||||
let shapes = this._shapesForMerge;
|
||||
|
||||
// Undo/redo code
|
||||
window.cvat.addAction('Merge Objects', () => {
|
||||
model.unsubscribe(this._collectionModel);
|
||||
model.removed = true;
|
||||
for (let shape of shapes) {
|
||||
shape.removed = false;
|
||||
shape.subscribe(this._collectionModel);
|
||||
}
|
||||
this._collectionModel.update();
|
||||
}, () => {
|
||||
for (let shape of shapes) {
|
||||
shape.removed = true;
|
||||
shape.unsubscribe(this._collectionModel);
|
||||
}
|
||||
model.subscribe(this._collectionModel);
|
||||
model.removed = false;
|
||||
}, window.cvat.player.frames.current);
|
||||
// End of undo/redo code
|
||||
|
||||
this.cancel();
|
||||
for (let shape of shapes) {
|
||||
shape.removed = true;
|
||||
shape.unsubscribe(this._collectionModel);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
cancel() {
|
||||
if (window.cvat.mode == 'merge') {
|
||||
window.cvat.mode = null;
|
||||
this._mergeMode = false;
|
||||
|
||||
for (let shape of this._shapesForMerge) {
|
||||
shape.merge = false;
|
||||
}
|
||||
this._shapesForMerge = [];
|
||||
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
|
||||
click() {
|
||||
if (this._mergeMode) {
|
||||
const active = this._collectionModel.selectShape(
|
||||
this._collectionModel.lastPosition,
|
||||
true,
|
||||
);
|
||||
if (active) {
|
||||
this._pushForMerge(active);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get mergeMode() {
|
||||
return this._mergeMode;
|
||||
}
|
||||
}
|
||||
|
||||
class ShapeMergerController {
|
||||
constructor(model) {
|
||||
this._model = model;
|
||||
|
||||
setupMergeShortkeys.call(this);
|
||||
function setupMergeShortkeys() {
|
||||
let shortkeys = window.cvat.config.shortkeys;
|
||||
|
||||
let switchMergeHandler = Logger.shortkeyLogDecorator(function() {
|
||||
this.switch();
|
||||
}.bind(this));
|
||||
|
||||
Mousetrap.bind(shortkeys["switch_merge_mode"].value, switchMergeHandler.bind(this), 'keydown');
|
||||
}
|
||||
}
|
||||
|
||||
switch() {
|
||||
if (this._model.mergeMode) {
|
||||
this._model.done();
|
||||
}
|
||||
else {
|
||||
this._model.start();
|
||||
}
|
||||
}
|
||||
|
||||
click() {
|
||||
this._model.click();
|
||||
}
|
||||
}
|
||||
|
||||
class ShapeMergerView {
|
||||
constructor(model, controller) {
|
||||
this._controller = controller;
|
||||
this._frameContent = $('#frameContent');
|
||||
this._mergeButton = $('#mergeTracksButton');
|
||||
this._mergeButton.on('click', () => controller.switch());
|
||||
|
||||
let shortkeys = window.cvat.config.shortkeys;
|
||||
this._mergeButton.attr('title', `
|
||||
${shortkeys['switch_merge_mode'].view_value} - ${shortkeys['switch_merge_mode'].description}`);
|
||||
|
||||
model.subscribe(this);
|
||||
}
|
||||
|
||||
onShapeMergerUpdate(shapeMerger) {
|
||||
if (shapeMerger.mergeMode) {
|
||||
this._mergeButton.text('Apply Merge');
|
||||
this._frameContent.on('click.merger', () => {
|
||||
this._controller.click();
|
||||
});
|
||||
}
|
||||
else {
|
||||
this._frameContent.off('click.merger');
|
||||
this._mergeButton.text('Merge Shapes');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,91 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* exported ShapeSplitter */
|
||||
"use strict";
|
||||
|
||||
class ShapeSplitter {
|
||||
_convertMutableAttributes(attributes) {
|
||||
const result = [];
|
||||
for (const attrId in attributes) {
|
||||
if (Object.prototype.hasOwnProperty.call(attributes, attrId)) {
|
||||
const attrInfo = window.cvat.labelsInfo.attrInfo(attrId);
|
||||
if (attrInfo.mutable) {
|
||||
result.push({
|
||||
id: +attrId,
|
||||
value: attributes[attrId].value,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
split(track, frame) {
|
||||
const keyFrames = track.keyframes.map(keyframe => +keyframe).sort((a, b) => a - b);
|
||||
const exported = track.export();
|
||||
|
||||
if (frame > +keyFrames[0]) {
|
||||
const curInterpolation = track.interpolate(frame);
|
||||
const prevInterpolation = track.interpolate(frame - 1);
|
||||
const curAttributes = this._convertMutableAttributes(curInterpolation.attributes);
|
||||
const prevAttrributes = this._convertMutableAttributes(prevInterpolation.attributes);
|
||||
const curPositionList = [];
|
||||
const prevPositionList = [];
|
||||
|
||||
for (const shape of exported.shapes) {
|
||||
if (shape.frame < frame - 1) {
|
||||
prevPositionList.push(shape);
|
||||
} else if (shape.frame > frame) {
|
||||
curPositionList.push(shape);
|
||||
}
|
||||
}
|
||||
|
||||
if (track.type.split('_')[1] === 'box') {
|
||||
const prevPos = prevInterpolation.position;
|
||||
prevPositionList.push(Object.assign({}, {
|
||||
frame: frame - 1,
|
||||
attributes: prevAttrributes,
|
||||
type: 'box',
|
||||
}, prevPos));
|
||||
|
||||
const curPos = curInterpolation.position;
|
||||
prevPositionList.push(Object.assign({}, {
|
||||
frame,
|
||||
attributes: curAttributes,
|
||||
type: 'box',
|
||||
}, curPos, { outside: true }));
|
||||
|
||||
curPositionList.push(Object.assign({}, {
|
||||
frame,
|
||||
attributes: curAttributes,
|
||||
type: 'box',
|
||||
}, curPos));
|
||||
} else {
|
||||
const curPos = curInterpolation.position;
|
||||
curPositionList.push(Object.assign({
|
||||
frame,
|
||||
attributes: curAttributes,
|
||||
type: track.type.split('_')[1],
|
||||
}, curPos));
|
||||
}
|
||||
|
||||
// don't clone id of splitted object
|
||||
delete exported.id;
|
||||
// don't clone group of splitted object
|
||||
delete exported.group;
|
||||
|
||||
const prevExported = JSON.parse(JSON.stringify(exported));
|
||||
const curExported = JSON.parse(JSON.stringify(exported));
|
||||
prevExported.shapes = prevPositionList;
|
||||
curExported.shapes = curPositionList;
|
||||
curExported.frame = frame;
|
||||
return [prevExported, curExported];
|
||||
}
|
||||
return [exported];
|
||||
}
|
||||
}
|
||||
@ -1,368 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* exported Config */
|
||||
|
||||
class Config {
|
||||
constructor() {
|
||||
this._username = '_default_';
|
||||
this._shortkeys = {
|
||||
switch_lock_property: {
|
||||
value: 'l',
|
||||
view_value: 'L',
|
||||
description: 'switch lock property for active shape',
|
||||
},
|
||||
|
||||
switch_all_lock_property: {
|
||||
value: 't l',
|
||||
view_value: 'T + L',
|
||||
description: 'switch lock property for all shapes on current frame',
|
||||
},
|
||||
|
||||
switch_occluded_property: {
|
||||
value: 'q,/'.split(','),
|
||||
view_value: 'Q or Num Division',
|
||||
description: 'switch occluded property for active shape',
|
||||
},
|
||||
|
||||
switch_draw_mode: {
|
||||
value: 'n',
|
||||
view_value: 'N',
|
||||
description: 'start draw / stop draw',
|
||||
},
|
||||
|
||||
switch_merge_mode: {
|
||||
value: 'm',
|
||||
view_value: 'M',
|
||||
description: 'start merge / apply changes',
|
||||
},
|
||||
|
||||
switch_group_mode: {
|
||||
value: 'g',
|
||||
view_value: 'G',
|
||||
description: 'start group / apply changes',
|
||||
},
|
||||
|
||||
reset_group: {
|
||||
value: 'shift+g',
|
||||
view_value: 'Shift + G',
|
||||
description: 'reset group for selected shapes',
|
||||
},
|
||||
|
||||
change_shape_label: {
|
||||
value: 'ctrl+1,ctrl+2,ctrl+3,ctrl+4,ctrl+5,ctrl+6,ctrl+7,ctrl+8,ctrl+9'.split(','),
|
||||
view_value: 'Ctrl + (1,2,3,4,5,6,7,8,9)',
|
||||
description: 'change shape label for existing object',
|
||||
},
|
||||
|
||||
change_default_label: {
|
||||
value: 'shift+1,shift+2,shift+3,shift+4,shift+5,shift+6,shift+7,shift+8,shift+9'.split(','),
|
||||
view_value: 'Shift + (1,2,3,4,5,6,7,8,9)',
|
||||
description: 'change label default label',
|
||||
},
|
||||
|
||||
change_shape_color: {
|
||||
value: 'enter',
|
||||
view_value: 'Enter',
|
||||
description: 'change color for highlighted shape',
|
||||
},
|
||||
|
||||
change_player_brightness: {
|
||||
value: 'shift+b,alt+b'.split(','),
|
||||
view_value: 'Shift+B / Alt+B',
|
||||
description: 'increase/decrease brightness of an image',
|
||||
},
|
||||
|
||||
change_player_contrast: {
|
||||
value: 'shift+c,alt+c'.split(','),
|
||||
view_value: 'Shift+C / Alt+C',
|
||||
description: 'increase/decrease contrast of an image',
|
||||
},
|
||||
|
||||
change_player_saturation: {
|
||||
value: 'shift+s,alt+s'.split(','),
|
||||
view_value: 'Shift+S / Alt+S',
|
||||
description: 'increase/decrease saturation of an image',
|
||||
},
|
||||
|
||||
switch_hide_mode: {
|
||||
value: 'h',
|
||||
view_value: 'H',
|
||||
description: 'switch hide mode for active shape',
|
||||
},
|
||||
|
||||
switch_active_keyframe: {
|
||||
value: 'k',
|
||||
view_value: 'K',
|
||||
description: 'switch keyframe property for active shape',
|
||||
},
|
||||
|
||||
switch_active_outside: {
|
||||
value: 'o',
|
||||
view_value: 'O',
|
||||
description: 'switch outside property for active shape',
|
||||
},
|
||||
|
||||
switch_all_hide_mode: {
|
||||
value: 't h',
|
||||
view_value: 'T + H',
|
||||
description: 'switch hide mode for all shapes',
|
||||
},
|
||||
|
||||
delete_shape: {
|
||||
value: 'del,shift+del'.split(','),
|
||||
view_value: 'Del, Shift + Del',
|
||||
description: 'delete active shape (use shift for force deleting)',
|
||||
},
|
||||
|
||||
focus_to_frame: {
|
||||
value: '`,~'.split(','),
|
||||
view_value: '~ / `',
|
||||
description: 'focus to "go to frame" element',
|
||||
},
|
||||
|
||||
next_frame: {
|
||||
value: 'f',
|
||||
view_value: 'F',
|
||||
description: 'move to next player frame',
|
||||
},
|
||||
|
||||
prev_frame: {
|
||||
value: 'd',
|
||||
view_value: 'D',
|
||||
description: 'move to previous player frame',
|
||||
},
|
||||
|
||||
forward_frame: {
|
||||
value: 'v',
|
||||
view_value: 'V',
|
||||
description: 'move forward several frames',
|
||||
},
|
||||
|
||||
backward_frame: {
|
||||
value: 'c',
|
||||
view_value: 'C',
|
||||
description: 'move backward several frames',
|
||||
},
|
||||
|
||||
next_key_frame: {
|
||||
value: 'r',
|
||||
view_value: 'R',
|
||||
description: 'move to next key frame of highlighted track',
|
||||
},
|
||||
|
||||
prev_key_frame: {
|
||||
value: 'e',
|
||||
view_value: 'E',
|
||||
description: 'move to previous key frame of highlighted track',
|
||||
},
|
||||
|
||||
prev_filter_frame: {
|
||||
value: 'left',
|
||||
view_value: 'Left Arrow',
|
||||
description: 'move to prev frame which satisfies the filter',
|
||||
},
|
||||
|
||||
next_filter_frame: {
|
||||
value: 'right',
|
||||
view_value: 'Right Arrow',
|
||||
description: 'move to next frame which satisfies the filter',
|
||||
},
|
||||
|
||||
play_pause: {
|
||||
value: 'space',
|
||||
view_value: 'Space',
|
||||
description: 'switch play / pause of player',
|
||||
},
|
||||
|
||||
open_help: {
|
||||
value: 'f1',
|
||||
view_value: 'F1',
|
||||
description: 'open help window',
|
||||
},
|
||||
|
||||
open_settings: {
|
||||
value: 'f2',
|
||||
view_value: 'F2',
|
||||
description: 'open settings window ',
|
||||
},
|
||||
|
||||
save_work: {
|
||||
value: 'ctrl+s',
|
||||
view_value: 'Ctrl + S',
|
||||
description: 'save work on the server',
|
||||
},
|
||||
|
||||
copy_shape: {
|
||||
value: 'ctrl+c',
|
||||
view_value: 'Ctrl + C',
|
||||
description: 'copy active shape to buffer',
|
||||
},
|
||||
|
||||
propagate_shape: {
|
||||
value: 'ctrl+b',
|
||||
view_value: 'Ctrl + B',
|
||||
description: 'propagate active shape',
|
||||
},
|
||||
|
||||
switch_paste: {
|
||||
value: 'ctrl+v',
|
||||
view_value: 'Ctrl + V',
|
||||
description: 'switch paste mode',
|
||||
},
|
||||
|
||||
switch_aam_mode: {
|
||||
value: 'shift+enter',
|
||||
view_value: 'Shift + Enter',
|
||||
description: 'switch attribute annotation mode',
|
||||
},
|
||||
|
||||
aam_next_attribute: {
|
||||
value: 'down',
|
||||
view_value: 'Down Arrow',
|
||||
description: 'move to next attribute in attribute annotation mode',
|
||||
},
|
||||
|
||||
aam_prev_attribute: {
|
||||
value: 'up',
|
||||
view_value: 'Up Arrow',
|
||||
description: 'move to previous attribute in attribute annotation mode',
|
||||
},
|
||||
|
||||
aam_next_shape: {
|
||||
value: 'tab',
|
||||
view_value: 'Tab',
|
||||
description: 'move to next shape in attribute annotation mode',
|
||||
},
|
||||
|
||||
aam_prev_shape: {
|
||||
value: 'shift+tab',
|
||||
view_value: 'Shift + Tab',
|
||||
description: 'move to previous shape in attribute annotation mode',
|
||||
},
|
||||
|
||||
select_i_attribute: {
|
||||
value: '1,2,3,4,5,6,7,8,9,0'.split(','),
|
||||
view_value: '1,2,3,4,5,6,7,8,9,0',
|
||||
description: 'setup corresponding attribute value in attribute annotation mode',
|
||||
},
|
||||
|
||||
change_grid_opacity: {
|
||||
value: ['alt+g+=', 'alt+g+-'],
|
||||
view_value: 'Alt + G + "+", Alt + G + "-"',
|
||||
description: 'increase/decrease grid opacity',
|
||||
},
|
||||
|
||||
change_grid_color: {
|
||||
value: 'alt+g+enter',
|
||||
view_value: 'Alt + G + Enter',
|
||||
description: 'change grid color',
|
||||
},
|
||||
|
||||
undo: {
|
||||
value: 'ctrl+z',
|
||||
view_value: 'Ctrl + Z',
|
||||
description: 'undo',
|
||||
},
|
||||
|
||||
redo: {
|
||||
value: ['ctrl+shift+z', 'ctrl+y'],
|
||||
view_value: 'Ctrl + Shift + Z / Ctrl + Y',
|
||||
description: 'redo',
|
||||
},
|
||||
|
||||
cancel_mode: {
|
||||
value: 'esc',
|
||||
view_value: 'Esc',
|
||||
description: 'cancel active mode',
|
||||
},
|
||||
|
||||
clockwise_rotation: {
|
||||
value: 'ctrl+r',
|
||||
view_value: 'Ctrl + R',
|
||||
description: 'clockwise image rotation',
|
||||
},
|
||||
|
||||
counter_clockwise_rotation: {
|
||||
value: 'ctrl+shift+r',
|
||||
view_value: 'Ctrl + Shift + R',
|
||||
description: 'counter clockwise image rotation',
|
||||
},
|
||||
|
||||
next_shape_type: {
|
||||
value: ['alt+.'],
|
||||
view_value: 'Alt + >',
|
||||
description: 'switch next default shape type',
|
||||
},
|
||||
|
||||
prev_shape_type: {
|
||||
value: ['alt+,'],
|
||||
view_value: 'Alt + <',
|
||||
description: 'switch previous default shape type',
|
||||
},
|
||||
};
|
||||
|
||||
if (window.cvat && window.cvat.job && window.cvat.job.z_order) {
|
||||
this._shortkeys.inc_z = {
|
||||
value: '+,='.split(','),
|
||||
view_value: '+',
|
||||
description: 'increase z order for active shape',
|
||||
};
|
||||
|
||||
this._shortkeys.dec_z = {
|
||||
value: '-,_'.split(','),
|
||||
view_value: '-',
|
||||
description: 'decrease z order for active shape',
|
||||
};
|
||||
}
|
||||
|
||||
this._settings = {
|
||||
player_step: {
|
||||
value: '10',
|
||||
description: 'step size for player when move on several frames forward/backward',
|
||||
},
|
||||
|
||||
player_speed: {
|
||||
value: '25 FPS',
|
||||
description: 'playback speed of the player',
|
||||
},
|
||||
|
||||
reset_zoom: {
|
||||
value: 'false',
|
||||
description: 'reset frame zoom when move between the frames',
|
||||
},
|
||||
|
||||
enable_auto_save: {
|
||||
value: 'false',
|
||||
description: 'enable auto save ability',
|
||||
},
|
||||
|
||||
auto_save_interval: {
|
||||
value: '15',
|
||||
description: 'auto save interval (min)',
|
||||
},
|
||||
};
|
||||
|
||||
this._defaultShortkeys = JSON.parse(JSON.stringify(this._shortkeys));
|
||||
this._defaultSettings = JSON.parse(JSON.stringify(this._settings));
|
||||
}
|
||||
|
||||
|
||||
reset() {
|
||||
this._shortkeys = JSON.parse(JSON.stringify(this._defaultShortkeys));
|
||||
this._settings = JSON.parse(JSON.stringify(this._defaultSettings));
|
||||
}
|
||||
|
||||
|
||||
get shortkeys() {
|
||||
return JSON.parse(JSON.stringify(this._shortkeys));
|
||||
}
|
||||
|
||||
|
||||
get settings() {
|
||||
return JSON.parse(JSON.stringify(this._settings));
|
||||
}
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* exported convertPlainArrayToActual, convertToArray, intersection */
|
||||
|
||||
/* global
|
||||
PolylineModel:false
|
||||
*/
|
||||
|
||||
// Takes a 2d array of canvas points and transforms it to an array of point objects
|
||||
function convertPlainArrayToActual(arr) {
|
||||
let actual = [{ x: arr[0], y: arr[1] }];
|
||||
actual = PolylineModel.convertNumberArrayToString(actual);
|
||||
actual = window.cvat.translate.points.canvasToActual(actual);
|
||||
actual = PolylineModel.convertStringToNumberArray(actual);
|
||||
return actual;
|
||||
}
|
||||
|
||||
// converts an array of point objects to a 2D array
|
||||
function convertToArray(points) {
|
||||
const arr = [];
|
||||
points.forEach((point) => {
|
||||
arr.push([point.x, point.y]);
|
||||
});
|
||||
return arr;
|
||||
}
|
||||
|
||||
|
||||
function line(p1, p2) {
|
||||
const a = p1[1] - p2[1];
|
||||
const b = p2[0] - p1[0];
|
||||
const c = b * p1[1] + a * p1[0];
|
||||
return [a, b, c];
|
||||
}
|
||||
|
||||
function intersection(p1, p2, p3, p4) {
|
||||
const L1 = line(p1, p2);
|
||||
const L2 = line(p3, p4);
|
||||
|
||||
const D = L1[0] * L2[1] - L1[1] * L2[0];
|
||||
const Dx = L1[2] * L2[1] - L1[1] * L2[2];
|
||||
const Dy = L1[0] * L2[2] - L1[2] * L2[0];
|
||||
|
||||
let x = null;
|
||||
let y = null;
|
||||
if (D !== 0) {
|
||||
x = Dx / D;
|
||||
y = Dy / D;
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@ -1,599 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* ----------------------- Classes ----------------------- */
|
||||
.playerButton {
|
||||
fill: #09c;
|
||||
stroke-width: 2px;
|
||||
stroke: black;
|
||||
margin-top: 1%;
|
||||
margin-right: 15px;
|
||||
float:left;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.playerButton:hover {
|
||||
fill: rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
.playerButton:active {
|
||||
fill: #017196;
|
||||
}
|
||||
|
||||
.disabledPlayerButton {
|
||||
fill:grey !important;
|
||||
stroke: grey !important;
|
||||
}
|
||||
|
||||
.graphicButton {
|
||||
opacity: 0.6;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: none;
|
||||
outline: none;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.graphicButton:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.graphicButton:active {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.lockButton {
|
||||
background: url("icons/unlock.png") no-repeat;
|
||||
background-size: 20px;
|
||||
}
|
||||
|
||||
.locked {
|
||||
background: url("icons/lock.png") no-repeat;
|
||||
background-size: 20px;
|
||||
}
|
||||
|
||||
.occludedButton {
|
||||
background: url("icons/non-occluded.png") no-repeat;
|
||||
background-size: 20px;
|
||||
}
|
||||
|
||||
.occluded {
|
||||
background: url("icons/occluded.png") no-repeat;
|
||||
background-size: 20px;
|
||||
}
|
||||
|
||||
.copyButton {
|
||||
background: url("icons/copy.png") no-repeat;
|
||||
background-size: 20px;
|
||||
}
|
||||
|
||||
.propagateButton {
|
||||
background: url("icons/propagate.png") no-repeat;
|
||||
background-size: 20px;
|
||||
}
|
||||
|
||||
.hiddenButton {
|
||||
background: url("icons/non-hidden.png") no-repeat;
|
||||
background-size: 20px;
|
||||
}
|
||||
|
||||
.hiddenText {
|
||||
background: url("icons/hidden-text.png") no-repeat;
|
||||
background-size: 20px;
|
||||
}
|
||||
|
||||
.hiddenShape {
|
||||
background: url("icons/hidden-shape.png") no-repeat;
|
||||
background-size: 20px;
|
||||
}
|
||||
|
||||
.outsideButton {
|
||||
background: url("icons/non-outside.png") no-repeat;
|
||||
background-size: 20px;
|
||||
}
|
||||
|
||||
.outside {
|
||||
background: url("icons/outside.png") no-repeat;
|
||||
background-size: 20px;
|
||||
}
|
||||
|
||||
.keyFrameButton {
|
||||
background: url("icons/non-keyframe.png") no-repeat;
|
||||
background-size: 20px;
|
||||
}
|
||||
|
||||
.keyFrame {
|
||||
background: url("icons/keyframe.png") no-repeat;
|
||||
background-size: 20px;
|
||||
}
|
||||
|
||||
.prevKeyFrameButton {
|
||||
background: url("icons/prevKeyFrame.png") no-repeat;
|
||||
background-size: 20px;
|
||||
}
|
||||
|
||||
.initKeyFrameButton {
|
||||
background: url("icons/initKeyFrame.png") no-repeat;
|
||||
background-size: 20px;
|
||||
}
|
||||
|
||||
.nextKeyFrameButton {
|
||||
background: url("icons/nextKeyFrame.png") no-repeat;
|
||||
background-size: 20px;
|
||||
}
|
||||
|
||||
|
||||
.uiElement, .labelContentElement {
|
||||
width: 93%;
|
||||
border-radius: 2px;
|
||||
margin: 1% auto;
|
||||
padding: 1%;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
border: 3px solid rgba(0,0,0,0);
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.labelContentElement:active {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.highlightedUI {
|
||||
border: 3px solid black;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.uiAttr {
|
||||
margin-left: 5%;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.selectAttr {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.numberAttr {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.textAttr {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.shape {
|
||||
position: relative;
|
||||
shape-rendering: geometricprecision;
|
||||
color-rendering: optimizeQuality;
|
||||
}
|
||||
|
||||
.selectedShape {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.polyline {
|
||||
fill: none !important;
|
||||
}
|
||||
|
||||
.points {
|
||||
stroke: none !important;
|
||||
fill: none !important;
|
||||
}
|
||||
|
||||
.shapeSelect {
|
||||
fill-opacity: 0;
|
||||
stroke-opacity: 1;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.shapeCreation {
|
||||
stroke: #fff;
|
||||
fill-opacity: 0;
|
||||
}
|
||||
|
||||
.svg_select_points {
|
||||
fill-opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.svg_select_points_lt{
|
||||
cursor: nw-resize;
|
||||
}
|
||||
.svg_select_points_rt{
|
||||
cursor: ne-resize;
|
||||
}
|
||||
.svg_select_points_rb{
|
||||
cursor: se-resize;
|
||||
}
|
||||
.svg_select_points_lb{
|
||||
cursor: sw-resize;
|
||||
}
|
||||
.svg_select_points_t{
|
||||
cursor: n-resize;
|
||||
}
|
||||
.svg_select_points_r{
|
||||
cursor: e-resize;
|
||||
}
|
||||
.svg_select_points_b{
|
||||
cursor: s-resize;
|
||||
}
|
||||
.svg_select_points_l{
|
||||
cursor: w-resize;
|
||||
}
|
||||
|
||||
.svg_select_points_ns{
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.svg_select_points_ew{
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.shape-creator-border-point {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.shape-creator-border-point:hover {
|
||||
opacity: 1;
|
||||
fill: red;
|
||||
}
|
||||
|
||||
.shape-creator-border-point:active {
|
||||
opacity: 0.55;
|
||||
fill: red;
|
||||
}
|
||||
|
||||
.shape-creator-border-point-direction {
|
||||
fill: blueviolet;
|
||||
}
|
||||
|
||||
.shapeText {
|
||||
font-size: 0.12em;
|
||||
fill: white;
|
||||
stroke:black;
|
||||
stroke-width: 0.05;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.highlightedShape {
|
||||
fill-opacity: 0.2;
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
|
||||
.occludedShape {
|
||||
stroke-dasharray: 5;
|
||||
}
|
||||
|
||||
.mergeShape {
|
||||
fill: blue;
|
||||
fill-opacity: 0.5;
|
||||
}
|
||||
|
||||
.mergePoints {
|
||||
fill: blue;
|
||||
fill-opacity: 0.5;
|
||||
}
|
||||
|
||||
.mergeLine {
|
||||
stroke: blue;
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
|
||||
.groupShape {
|
||||
fill: darkmagenta;
|
||||
fill-opacity: 0.5;
|
||||
}
|
||||
|
||||
.groupPoints {
|
||||
fill: darkmagenta;
|
||||
fill-opacity: 0.5;
|
||||
}
|
||||
|
||||
.groupLine {
|
||||
stroke: darkmagenta;
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
|
||||
.lockedShape {
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
.lockedText {
|
||||
fill-opacity: 0.5;
|
||||
}
|
||||
|
||||
|
||||
.menuButton {
|
||||
width: 90%;
|
||||
height: 40px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
|
||||
.settingsBox {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.custom-menu {
|
||||
display: none;
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
border: 1px solid #CCC;
|
||||
white-space: nowrap;
|
||||
font-family: sans-serif;
|
||||
background: #FFF;
|
||||
color: #333;
|
||||
border-radius: 5px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.buttonBlockOfLabelUI {
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
border: 1px solid black;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Each of the items in the list */
|
||||
.custom-menu li {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
list-style-type: none;
|
||||
transition: all .3s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.custom-menu li:hover {
|
||||
background-color: #DEF;
|
||||
}
|
||||
|
||||
.customizedTab {
|
||||
border-radius: 5px 5px 0px 0px;
|
||||
width: 100%;
|
||||
width: 15%;
|
||||
float: left;
|
||||
margin: 0px 10px;
|
||||
}
|
||||
|
||||
/* ----------------------- IDs ----------------------- */
|
||||
#helpWindowContent, #settingsWindowContent, #colorUIWindowContent {
|
||||
width: 700px;
|
||||
height: 450px;
|
||||
}
|
||||
|
||||
#annotationMenu
|
||||
{
|
||||
background-color: #B0C4DE;
|
||||
padding: 8px 0;
|
||||
height: auto;
|
||||
max-width: 1000px;
|
||||
z-index: 100;
|
||||
float: left;
|
||||
position: absolute;
|
||||
border-radius: 5px;
|
||||
border: 1px solid black;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#taskAnnotationCenterPanel {
|
||||
width: 83%;
|
||||
height: 100%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#taskAnnotationRightPanel {
|
||||
height: 100%;
|
||||
width: 15%;
|
||||
float: left;
|
||||
margin: 0px 10px;
|
||||
}
|
||||
|
||||
#uiContent, #trackManagement, #aamMenu, #labelsContent {
|
||||
border-bottom: 1px solid black;
|
||||
border-right: 1px solid black;
|
||||
border-left: 1px solid black;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 5px rgba(0,0,0,0.5);
|
||||
background-color: #B0C4DE;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#shapeLabelSelector, #shapeModeSelector, #shapeTypeSelector, polyShapeSizeWrapper {
|
||||
width: 80%;
|
||||
margin-left: auto;
|
||||
margin-top: 2%;
|
||||
}
|
||||
|
||||
#trackManagement, #aamMenu {
|
||||
padding: 1em 0;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
margin: 10px 0px;
|
||||
}
|
||||
|
||||
#aamMenu {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#aamHelpContainer {
|
||||
text-align: left;
|
||||
margin-left: 5px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#uiContent, #labelsContent {
|
||||
height: 75.4%;
|
||||
width: 100%;
|
||||
border-radius: 0px 0px 5px 5px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#player {
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#playerFrame {
|
||||
width: 100%;
|
||||
height: 80%;
|
||||
border: 1px black solid;
|
||||
border-radius: 5px;
|
||||
background-color: #B0C4DE;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#rotationWrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
#frameContent {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
outline: 10px solid black;
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
#frameText {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
transform-origin: center center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#frameGrid {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
transform-origin: top left;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#frameBackground {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
background-repeat: no-repeat;
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
#canvasBackground {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
background-repeat: no-repeat;
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
#frameLoadingAnimation {
|
||||
fill-opacity: 0;
|
||||
stroke: #09c;
|
||||
stroke-width: 3px;
|
||||
stroke-dasharray: 50;
|
||||
animation: dash 1s linear infinite;
|
||||
}
|
||||
|
||||
#frameLoadingAnim {
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
#annotationStatisticTable {
|
||||
margin: 0px auto;
|
||||
border: 1px solid dimgrey;
|
||||
border-radius: 6px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
border-collapse: separate;
|
||||
border-spacing: 3px;
|
||||
}
|
||||
|
||||
#annotationStatisticTable th, #annotationStatisticTable td { padding: 3px; }
|
||||
|
||||
#annotationSettingsTable th, #annotationSettingsTable td {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#playerPanel {
|
||||
width: 100%;
|
||||
height: 5%;
|
||||
}
|
||||
|
||||
#playerSettings {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#playerStep {
|
||||
width: 2em;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#filterInputString {
|
||||
margin-right: 10px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
|
||||
#playerProgress {
|
||||
-webkit-appearance: none;
|
||||
margin-top: 1.1%;
|
||||
background-color: rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
#playerProgress::-webkit-slider-runnable-track {
|
||||
height: 70%;
|
||||
cursor: pointer;
|
||||
background: #09c;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #010101;
|
||||
}
|
||||
|
||||
#playerProgress:focus {
|
||||
outline-color: transparent;
|
||||
outline-style: none;
|
||||
}
|
||||
|
||||
#playerProgress::-webkit-slider-thumb {
|
||||
border: 1px solid #000000;
|
||||
height: 36px;
|
||||
width: 16px;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
cursor: pointer;
|
||||
-webkit-appearance: none;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
#playerProgress::-moz-range-track {
|
||||
height: 90%;
|
||||
cursor: pointer;
|
||||
background: #09c;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #010101;
|
||||
}
|
||||
|
||||
#playerProgress::-moz-range-thumb {
|
||||
border: 1px solid #000000;
|
||||
height: 36px;
|
||||
width: 16px;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#playerProgress::-moz-focus-outer { border: 0; }
|
||||
|
||||
|
||||
@keyframes dash {
|
||||
0% {stroke-dashoffset: 1; stroke: #09c;}
|
||||
50% {stroke-dashoffset: 100; stroke: #f44;}
|
||||
100% {stroke-dashoffset: 300; stroke: #09c;}
|
||||
}
|
||||
@ -1,473 +0,0 @@
|
||||
<!--
|
||||
Copyright (C) 2018-2020 Intel Corporation
|
||||
|
||||
SPDX-License-Identifier: MIT
|
||||
-->
|
||||
{% extends 'engine/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block head_css %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'engine/stylesheet.css' %}">
|
||||
{% for css_file in css_3rdparty %}
|
||||
<link rel="stylesheet" type="text/css" src="{% static css_file %}">
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block head_js_3rdparty %}
|
||||
{{ block.super }}
|
||||
<script type="text/javascript" src="{% static 'engine/js/3rdparty/svg.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/3rdparty/svg.draw.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/3rdparty/svg.resize.min.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/3rdparty/svg.draggable.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/3rdparty/svg.select.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/3rdparty/defiant.min.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/3rdparty/jquery-3.3.1.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/3rdparty/js.cookie.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/3rdparty/jquery.fullscreen.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/cvat-core.min.js' %}"></script>
|
||||
{% for js_file in js_3rdparty %}
|
||||
<script type="text/javascript" src="{% static js_file %}"></script>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block head_js_cvat %}
|
||||
{{ block.super }}
|
||||
<script type="text/javascript" src="{% static 'engine/js/utils.js' %}"></script>
|
||||
<script type="text/javascript">
|
||||
window.UI_URL = "{{ ui_url }}";
|
||||
</script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/logger.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/server.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/listener.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/history.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/borderSticker.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/coordinateTranslator.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/labelsInfo.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/annotationParser.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/attributeAnnotationMode.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/shapeFilter.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/shapeSplitter.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/polyshapeEditor.js' %}"></script>
|
||||
|
||||
<script type="text/javascript" src="{% static 'engine/js/bootstrap.js' %}"></script>
|
||||
|
||||
<script type="text/javascript" src="{% static 'engine/js/shapes.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/cuboidShape.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/shapeCollection.js' %}"></script>
|
||||
|
||||
<script type="text/javascript" src="{% static 'engine/js/player.js' %}"></script>
|
||||
|
||||
<script type="text/javascript" src="{% static 'engine/js/shapeMerger.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/shapeCreator.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/shapeBuffer.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/shapeGrouper.js' %}"></script>
|
||||
|
||||
<script type="text/javascript" src="{% static 'engine/js/annotationSaver.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/annotationUI.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div id="taskAnnotationCenterPanel">
|
||||
<div id="player">
|
||||
<div id="playerFrame">
|
||||
<div id="rotationWrapper">
|
||||
<svg id="frameLoadingAnim" style="width: 100%; height: 100%;" class="hidden">
|
||||
<circle r="30" cx="50%" cy="50%" id="frameLoadingAnimation"/>
|
||||
</svg>
|
||||
<svg id="frameContent"> </svg>
|
||||
<svg id="frameText"> </svg>
|
||||
<svg id="frameBackground"> </svg>
|
||||
<canvas id="canvasBackground"> </canvas>
|
||||
<svg id="frameGrid" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="playerGridPattern" width="100" height="100" patternUnits="userSpaceOnUse">
|
||||
<!-- Max size value for grid is 1000. Path size should be >= such value in order to it displayed correct -->
|
||||
<path id="playerGridPath" d="M 1000 0 L 0 0 0 1000" fill="none" stroke="white" opacity="0" stroke-width="2"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#playerGridPattern)" />
|
||||
</svg>
|
||||
|
||||
<ul id="shapeContextMenu" class='custom-menu' oncontextmenu="return false;">
|
||||
<li action="object_url"> Copy Object URL </li>
|
||||
<li action="change_color"> Change Color </li>
|
||||
<li action="remove_shape"> Remove Shape </li>
|
||||
<li action="switch_occluded"> Switch Occluded </li>
|
||||
<li action="switch_lock"> Switch Lock </li>
|
||||
<li class="interpolationItem" action="split_track"> Split </li>
|
||||
<li class="polygonItem" action="drag_polygon"> Enable Dragging </li>
|
||||
<li class="cuboidItem" action="reset_perspective"> Reset Perspective </li>
|
||||
<li class="cuboidItem" action="switch_orientation"> Switch Perspective Orientation </li>
|
||||
|
||||
</ul>
|
||||
|
||||
<ul id="playerContextMenu" class='custom-menu' oncontextmenu="return false;">
|
||||
<li action="job_url"> Copy Job URL </li>
|
||||
<li action="frame_url"> Copy Frame URL </li>
|
||||
</ul>
|
||||
|
||||
<ul id="pointContextMenu" class='custom-menu' oncontextmenu="return false;">
|
||||
<li action="remove_point"> Remove </li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div id="playerPanel">
|
||||
<svg id="firstButton" class="playerButton">
|
||||
<polygon points="100,0 100,80 75,60 75,80 50,60, 50,80 0,40 50,0 50,20 75,0 75,20" transform="scale(0.4)"/>
|
||||
</svg>
|
||||
<svg id="multiplePrevButton" class="playerButton">
|
||||
<polygon points="100,0 100,80 75,60 75,80 25,40 75,0 75,20" transform="scale(0.4)"/>
|
||||
</svg>
|
||||
|
||||
<svg id="prevButton" class="playerButton">
|
||||
<polygon points="90,20 90,60 50,60 50,80 10,40 50,0 50,20" transform="scale(0.4)"/>
|
||||
</svg>
|
||||
|
||||
<svg id="playButton" class="playerButton">
|
||||
<polygon points="35,0 35,80 85,40" transform="scale(0.4)"/>
|
||||
</svg>
|
||||
|
||||
<svg id="pauseButton" class="playerButton hidden">
|
||||
<rect x="25" y="0" width="20" height="80" transform="scale(0.4)" />
|
||||
<rect x="65" y="0" width="20" height="80" transform="scale(0.4)" />
|
||||
</svg>
|
||||
|
||||
<svg id="nextButton" class="playerButton">
|
||||
<polygon points="10,20 10,60 50,60 50,80 90,40 50,0 50,20" transform="scale(0.4)"/>
|
||||
</svg>
|
||||
|
||||
<svg id="multipleNextButton" class="playerButton">
|
||||
<polygon points="1,1 1,80 25,60 25,80 75,40 25,0 25,20" transform="scale(0.4)"/>
|
||||
</svg>
|
||||
|
||||
<svg id="lastButton" class="playerButton">
|
||||
<polygon points="1,1 1,80 25,60 25,80 50,60 50,80 100,40 50,0 50,20 25,0 25,20" transform="scale(0.4)"/>
|
||||
</svg>
|
||||
<input type = "range" id = "playerProgress"/>
|
||||
</div> <!-- END of PLAYER PANEL -->
|
||||
<div style="margin-top: 20px">
|
||||
<button id="menuButton" class="regular h2"> Open Menu </button>
|
||||
<label class="regular h2" style="margin-left: 50px"> Filter: </label>
|
||||
<datalist id="filterSubmitList" style="display: none;"> </datalist>
|
||||
<input type="text" list="filterSubmitList" id="filterInputString" class="regular h2" placeholder='car[attr/model=/"mazda"'/>
|
||||
<button id="resetFilterButton" class="regular h2"> Reset </button>
|
||||
<button class="regular h2" id="undoButton" disabled> ⟲ </button>
|
||||
<select size="2" class="regular" style="overflow: hidden; width: 15%; top: 0.5em; position: relative;" disabled>
|
||||
<option id="lastUndoText" title="Undo Action" selected> None </option>
|
||||
<option id="lastRedoText" title="Redo Action"> None </option>
|
||||
</select>
|
||||
<button class="regular h2" id="redoButton" disabled> ⟳ </button>
|
||||
<label class="regular h2" style="margin-left: 15px;"> Propagation </label>
|
||||
<input type ="number" id="propagateFramesInput" style="width: 3em" min="1" max="10000" value="50" class="regular h2"/>
|
||||
<label class="regular h2" style="margin-left: 15px;"> Rotation </label>
|
||||
<button class="regular h2" id="clockwiseRotation" title="Clockwise rotation"> ⟳ </button>
|
||||
<button class="regular h2" id="counterClockwiseRotation" title="Counter clockwise rotation"> ⟲ </button>
|
||||
<div style="float: right;">
|
||||
<label class="regular h2"> Frame </label>
|
||||
<input class="regular h2" style="width: 3.5em;" type="number" id="frameNumber">
|
||||
</div>
|
||||
<hr>
|
||||
<table id="annotationSettingsTable" class="regular">
|
||||
<tr>
|
||||
<td style="width: auto;">
|
||||
<div style="float: left;"> <label class="semiBold"> Fill Opacity: </label> </div>
|
||||
<div style="float: left; margin-left: 1em;"> <input type="range" min="-1" max="1" step="0.2" value="0" id="fillOpacityRange"/> </div>
|
||||
</td>
|
||||
<td style="width: auto;">
|
||||
<div style="float: left;"> <label class="semiBold"> Selected Fill Opacity: </label> </div>
|
||||
<div style="float: left; margin-left: 1em;"> <input type="range" min="0" max="1" value="0.2" step="0.2" id="selectedFillOpacityRange"/> </div>
|
||||
</td>
|
||||
<td style="width: auto;">
|
||||
<div style="float: left;"> <label class="semiBold"> Black Stroke: </label> </div>
|
||||
<div style="float: left; margin-left: 1em;"> <input type="checkbox" id="blackStrokeCheckbox" class="settingsBox"/> </div>
|
||||
</td>
|
||||
<td style="width: auto;">
|
||||
<div style="float: left;"> <label class="semiBold"> Color by: </label> </div>
|
||||
<div style="float: left; margin-left: 10px;">
|
||||
<label style="margin-right: 10px;"> Instance </label>
|
||||
<input type="radio" name="colorByRadio" id="colorByInstanceRadio" checked class="settingsBox"/>
|
||||
</div>
|
||||
<div style="float: left; margin-left: 10px;">
|
||||
<label style="margin-right: 10px;"> Group </label>
|
||||
<input type="radio" name="colorByRadio" id="colorByGroupRadio" class="settingsBox"/>
|
||||
</div>
|
||||
<div style="float: left; margin-left: 10px;">
|
||||
<label style="margin-right: 10px;"> Label </label>
|
||||
<input type="radio" name="colorByRadio" id="colorByLabelRadio" class="settingsBox"/>
|
||||
</div>
|
||||
<div style="float: left; margin-left: 50px;" title="Press Ctrl to switch">
|
||||
<label style="display: none;">
|
||||
Common borders
|
||||
<input type="checkbox" id="commonBordersCheckbox" class="settingsBox" style="margin-left: 10px; display: none;"/>
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div> <!-- END of PLAYER -->
|
||||
|
||||
<div id="helpWindow" class="modal hidden">
|
||||
<div id="helpWindowContent" class="modal-content">
|
||||
<div style="width: 100%; height: 90%; overflow-y: auto;">
|
||||
<div class="selectable">
|
||||
<label class="h1 semiBold"> Shortkeys: </label> <br>
|
||||
<table class="regular" id="shortkeyHelpTable"> </table>
|
||||
<label class="h1 semiBold"> <br> Hints: <br> </label>
|
||||
<label class="regular"> - Hold MOUSEWHEEL in order to move frame (during drawing for example). </label> <br>
|
||||
<label class="regular"> - Hold CTRL key when track highlighted and fix it. </label> <br>
|
||||
<label class="regular"> - Hold CTRL key when paste shape from buffer for multiple pasting. </label> <br>
|
||||
<label class="h1 semiBold"> <br> Filter Help: <br> </label>
|
||||
<label class="regular">
|
||||
Filter Format: label[property operator "value"] (USE LOWER CASE ONLY) <br>
|
||||
Label is a type of bounding box (car, person, etc). Use "*" for any label. <br>
|
||||
Property is a limited set of values: id, type, lock, occluded, attr. <br>
|
||||
Operator is "=", "!=" for any properties and ">", "<", ">=", "<=" for numeric properties. <br>
|
||||
For complex conditions please use 'or', 'and' (for properties) "|" (for labels) operators. <br> <br>
|
||||
Examples: <br>
|
||||
*[mode="annotation"] - only annotation objects <br>
|
||||
*[type="polygon"] - only polygon objects <br>
|
||||
car[occluded="true"] - only occluded cars <br>
|
||||
*[lock!="true"] - only unlocked tracks <br>
|
||||
person[attr/age>="25" and attr/age<="35"] - persons with age (number) between [25,40] years <br>
|
||||
car[attr/parked="true"] - only parked cars <br>
|
||||
person[attr/race="asian"] | car[attr/model="bmw"] - asians and BMW cars <br>
|
||||
face[attr/glass="sunglasses" or attr/glass="no"] - faces with sunglasses or without glass <br>
|
||||
*[attr/*="__undefined__"] - any tracks with any unlabeled attributes <br>
|
||||
*[width<300 or height<300] - shapes with height or width less than 300px <br>
|
||||
person[width>300 and height<200] - person shapes with width > 300px and height < 200px <br>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<center> <button id="closeHelpButton" class="regular h1" style="margin-top: 15px;"> Close </button> </center>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settingsWindow" class="modal hidden">
|
||||
<div id="settingsWindowContent" class="modal-content">
|
||||
<div id="playerSettings" style="width: 48%; height: 90%; float: left;">
|
||||
<center> <label class="semiBold h1"> Player Settings </label> </center>
|
||||
<table style="border-collapse: separate; border-spacing: 10px; overflow-y: auto;" class="regular">
|
||||
<tr>
|
||||
<td> <label> Player Step </label> </td>
|
||||
<td> <input type="number" min="1" max="100" value="10" id="playerStep" class="regular h2"/> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> <label> Player Speed </label> </td>
|
||||
<td>
|
||||
<select id="speedSelect" class="regular h3">
|
||||
<option value="1"> 1 FPS </option>
|
||||
<option value="2"> 5 FPS </option>
|
||||
<option value="3"> 12 FPS </option>
|
||||
<option value="4" selected> 25 FPS </option>
|
||||
<option value="5"> 50 FPS </option>
|
||||
<option value="6"> 100 FPS </option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> <label> Reset Zoom </label> </td>
|
||||
<td> <input type="checkbox" id="resetZoomBox" class="settingsBox"/> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> <label> Grid Size </label> </td>
|
||||
<td> <input type="number" min="5" max="1000" value="100" id="playerGridSizeInput" class="regular h2"/> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> <label> Grid Opacity </label> </td>
|
||||
<td> <input type="range" min="0" max="5" value="0" id="playerGridOpacityInput" class="regular h2"/> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> <label> Grid Stroke </label> </td>
|
||||
<td>
|
||||
<select id="playerGridStrokeInput" class="regular h2">
|
||||
<option value="black"> Black </option>
|
||||
<option value="red"> Red </option>
|
||||
<option value="green"> Green </option>
|
||||
<option value="blue"> Blue </option>
|
||||
<option value="white" selected> White </option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> <label for="playerBrightnessRange"> Brightness: </label> </td>
|
||||
<td> <input type="range" min="50" max="200" value="100" id="playerBrightnessRange"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> <label for="playerContrastRange"> Contrast: </label> </td>
|
||||
<td> <input type="range" min="50" max="200" value="100" id="playerContrastRange"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> <label for="playerSaturationRange"> Saturation: </label> </td>
|
||||
<td> <input type="range" min="0" max="300" value="100" id="playerSaturationRange"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<button id="resetPlayerFilterButton" class="regular h1"> Reset Color Settings </button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="otherSettigns" style="width: 48%; height: 90%; float: left;">
|
||||
<center> <label class="semiBold h1"> Other Settings </label> </center>
|
||||
<table style="border-collapse: separate; border-spacing: 10px; overflow-y: auto;" class="regular">
|
||||
<tr>
|
||||
<td> <label> Show All Interpolation Tracks </label> </td>
|
||||
<td> <input type = "checkbox" id="showAllInterBox" class="settingsBox"/> </td>
|
||||
</tr>
|
||||
<tr >
|
||||
<td> <label> AAM Zoom Margin </label> </td>
|
||||
<td> <input type="range" min="0" max="1000" value="100" id="aamZoomMargin"/> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> <label> Enable Auto Saving </label> </td>
|
||||
<td> <input type = "checkbox" id="autoSaveBox" class="settingsBox"/> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> <label> Auto Saving Interval (Min) </label> </td>
|
||||
<td> <input type = "number" id="autoSaveTime" style="width: 3em" min="5" max="60" value="15" class="regular h2"/> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> <label> Rotate All Images </label> </td>
|
||||
<td> <input type = "checkbox" id="rotateAllImages" class="settingsBox"/> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> <label> Cuboid Projection Lines: </label> </td>
|
||||
<td><input type="checkbox" id="projectionLineEnable" class="settingsBox"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<center>
|
||||
<button id="closeSettignsButton" class="regular h1" style="margin-top: 15px;"> Close </button>
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="annotationMenu" class="hidden regular">
|
||||
<center style="float:left; width: 28%; height: 100%;" id="engineMenuButtons">
|
||||
<select id="downloadAnnotationButton" class="menuButton semiBold h2" style="text-align-last: center;">
|
||||
<option selected disabled> Dump Annotation </option>
|
||||
</select>
|
||||
|
||||
<select id="uploadAnnotationButton" class="menuButton semiBold h2" style="text-align-last: center;">
|
||||
<option selected disabled> Upload Annotation </option>
|
||||
</select>
|
||||
|
||||
<button id="openTaskButton" class="menuButton semiBold h2"> Open Task </button>
|
||||
<button id="removeAnnotationButton" class="menuButton semiBold h2"> Remove Annotation </button>
|
||||
<button id="settingsButton" class="menuButton semiBold h2"> Settings </button>
|
||||
<button id="fullScreenButton" class="menuButton semiBold h2"> Fullscreen Player </button>
|
||||
<button id="switchAAMButton" class="menuButton semiBold h2"> Switch AAM </button>
|
||||
<button id="helpButton" class="menuButton semiBold h2"> Help </button>
|
||||
<button id="saveButton" class="menuButton semiBold h2"> Save Work </button>
|
||||
<input type="file" id="annotationFileSelector" style="display: none"/>
|
||||
</center>
|
||||
<div style="float:left; width: 70%; height: 100%; text-align: left;" class="selectable">
|
||||
<center>
|
||||
<label id="statTaskName" class="semiBold h2"> </label> <br>
|
||||
<center>
|
||||
<select id="statTaskStatus" class="regular h2" style="outline: none; border-radius: 10px; background:#B0C4DE; border: 1px solid black;">
|
||||
{% for status in status_list %}
|
||||
<option value="{{status}}"> {{status}} </option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</center>
|
||||
</center>
|
||||
<center>
|
||||
<table style="width: 100%">
|
||||
<tr>
|
||||
<td style="width: 30%;">
|
||||
<label class="regular h2"> Frames: </label>
|
||||
<label id="statFrames" class="regular h2"> </label>
|
||||
</td>
|
||||
<td style="width: 30%;">
|
||||
<label class="regular h2"> Overlap: </label>
|
||||
<label id="statOverlap" class="regular h2"> </label>
|
||||
</td>
|
||||
<td style="width: 30%;">
|
||||
<label class="regular h2"> Z-Order: </label>
|
||||
<label id="statZOrder" class="regular h2"> </label>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</center>
|
||||
<center> <label class="semiBold h2"> Segment Statistic </label> </center>
|
||||
<div style="text-align: center; max-height: 250px; overflow: auto; display: block;">
|
||||
<table id="annotationStatisticTable">
|
||||
<tr class="semiBold">
|
||||
<td> Label </td>
|
||||
<td colspan="2"> Boxes </td>
|
||||
<td colspan="2"> Polygons </td>
|
||||
<td colspan="2"> Polylines </td>
|
||||
<td colspan="2"> Points </td>
|
||||
<td colspan="2"> Cuboids </td>
|
||||
<td> Manually </td>
|
||||
<td> Interpolated </td>
|
||||
<td> Total </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </td> <!-- Empty -->
|
||||
<td> S </td>
|
||||
<td> T </td>
|
||||
<td> S </td>
|
||||
<td> T </td>
|
||||
<td> S </td>
|
||||
<td> T </td>
|
||||
<td> S </td>
|
||||
<td> T </td>
|
||||
<td> S </td>
|
||||
<td> T </td>
|
||||
<td> </td> <!-- Empty -->
|
||||
<td> </td> <!-- Empty -->
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="tab customizedTab">
|
||||
<button class="h2 regular activeTabButton" id="sidePanelObjectsButton" style="width: 50%">Objects</button>
|
||||
<button class="h2 regular" id="sidePanelLabelsButton" style="width: 50%">Labels</button>
|
||||
</div>
|
||||
<div id="taskAnnotationRightPanel">
|
||||
<div id="uiContent"> </div>
|
||||
<div id="labelsContent" class="hidden"> </div>
|
||||
<div id="trackManagement">
|
||||
<button id="createShapeButton" class="regular h2" style="width: 80%;"> Create Shape </button>
|
||||
<button id="mergeTracksButton" class="regular h2" style="width: 80%; margin-top: 5px;"> Merge Shapes </button>
|
||||
<button id="groupShapesButton" class="regular h2" style="width: 80%; margin-top: 5px;"> Group Shapes </button>
|
||||
<select id="shapeLabelSelector" class="regular h2"> </select>
|
||||
<select id="shapeModeSelector" class="regular h2">
|
||||
<option value="annotation" class="regular"> Annotation </option>
|
||||
<option value="interpolation" class="regular"> Interpolation </option>
|
||||
</select>
|
||||
<select id="shapeTypeSelector" class="regular h2">
|
||||
<option value="box" class="regular" selected> Box </option>
|
||||
<option value="box_by_4_points" class="regular"> Box by 4 points </option>
|
||||
<option value="polygon" class="regular"> Polygon </option>
|
||||
<option value="polyline" class="regular"> Polyline </option>
|
||||
<option value="points" class="regular"> Points </option>
|
||||
<option value="cuboid" class="regular"> Cuboid </option>
|
||||
</select>
|
||||
<div id="polyShapeSizeWrapper">
|
||||
<label for="polyShapeSize" class="regular h2"> Poly Shape Size: </label>
|
||||
<input id="polyShapeSize" type="text" value="" class="regular h2" style="width: 30%; margin-top: 1%;" placeholder="0-100"/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="aamMenu" class="hidden">
|
||||
<label class="regular h1"> AAM Mode </label>
|
||||
<label id="aamCounter" class="regular h1"> </label> <br>
|
||||
<label id="aamTitle" class="regular h2"> </label>
|
||||
<div id="aamHelpContainer" class="regular">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -1,90 +0,0 @@
|
||||
<!--
|
||||
Copyright (C) 2018 Intel Corporation
|
||||
|
||||
SPDX-License-Identifier: MIT
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
{% load static compress %}
|
||||
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>
|
||||
{% block head_title %}
|
||||
Computer Vision Annotation Tool (CVAT)
|
||||
{% endblock %}
|
||||
</title>
|
||||
|
||||
{% compress css %}
|
||||
{% block head_css %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'engine/base.css' %}">
|
||||
{% endblock %}
|
||||
{% endcompress %}
|
||||
|
||||
{% compress js file platformchecker %}
|
||||
<script type="text/javascript" src="{% static 'engine/js/3rdparty/platform.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/checkPlatform.js' %}"></script>
|
||||
{% endcompress %}
|
||||
|
||||
{% compress js file thirdparty %}
|
||||
{% block head_js_3rdparty %}
|
||||
<script type="text/javascript" src="{% static 'engine/js/3rdparty/jquery-3.3.1.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/3rdparty/js.cookie.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/3rdparty/mousetrap.js' %}"></script>
|
||||
{% endblock %}
|
||||
{% endcompress %}
|
||||
|
||||
{% compress js file cvat %}
|
||||
{% block head_js_cvat %}
|
||||
<script type="text/javascript" src="{% static 'engine/js/base.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'engine/js/userConfig.js' %}"></script>
|
||||
{% endblock %}
|
||||
{% endcompress %}
|
||||
</head>
|
||||
<body>
|
||||
{% block header %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
{% endblock %}
|
||||
|
||||
<svg class="overlay" id="loadingOverlay" style="display: block;">
|
||||
<text class="semiBold" font-size="50" fill="#09c" x="44%" y="46%"> LOADING </text>
|
||||
</svg>
|
||||
</body>
|
||||
|
||||
<template id="confirmTemplate">
|
||||
<div class="modal">
|
||||
<div class="modal-content" style="width: 400px; height: auto; max-height: 600px; overflow: auto;">
|
||||
<label class="regular templateMessage selectable"> </label>
|
||||
<center>
|
||||
<button class="regular h2 templateDisagreeButton" style="margin-top: 20px"> Cancel </button>
|
||||
<button class="regular h2 templateAgreeButton" style="margin-top: 20px"> Ok </button>
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<template id="messageTemplate">
|
||||
<div class="modal">
|
||||
<div class="modal-content" style="width: 400px; height: auto; max-height: 600px; overflow: auto;">
|
||||
<label class="regular templateMessage selectable" style="word-break: break-word;"> </label>
|
||||
<center>
|
||||
<button class="regular h2 templateOKButton" style="margin-top: 20px"> Ok </button>
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="overlayTemplate">
|
||||
<div class="modal" style="display: block !important;">
|
||||
<div class="modal-content" style="width: 400px; height: auto; max-height: 600px; overflow: auto;">
|
||||
<label class="regular templateMessage selectable"> </label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</html>
|
||||
@ -1,3 +0,0 @@
|
||||
# Copyright (C) 2018 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
@ -1,3 +0,0 @@
|
||||
# Copyright (C) 2020 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
@ -1,131 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
'env': {
|
||||
'browser': true,
|
||||
'es6': true,
|
||||
'jquery': true,
|
||||
'qunit': true,
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
'rules': {
|
||||
'indent': [
|
||||
'warn',
|
||||
4
|
||||
],
|
||||
'linebreak-style': [
|
||||
'error',
|
||||
'unix'
|
||||
],
|
||||
'semi': [
|
||||
'warn'
|
||||
],
|
||||
'no-extra-semi': [
|
||||
'warn'
|
||||
],
|
||||
'no-console': [
|
||||
'warn'
|
||||
],
|
||||
},
|
||||
'globals': {
|
||||
// from attributeAnnotationMode.js
|
||||
'AAMController': true,
|
||||
'AAMModel': true,
|
||||
'AAMView': true,
|
||||
'AAMUndefinedKeyword': true,
|
||||
// from annotationParser.js
|
||||
'AnnotationParser': true,
|
||||
// from annotationUI.js
|
||||
'callAnnotationUI': true,
|
||||
'blurAllElements': true,
|
||||
'drawBoxSize': true,
|
||||
'copyToClipboard': true,
|
||||
// from base.js
|
||||
'showMessage': true,
|
||||
'showOverlay': true,
|
||||
'confirm': true,
|
||||
'dumpAnnotationRequest': true,
|
||||
'createExportContainer': true,
|
||||
'ExportType': true,
|
||||
'getExportTargetContainer': true,
|
||||
// from idGenerator.js
|
||||
'IncrementIdGenerator': true,
|
||||
'ConstIdGenerator': true,
|
||||
// from shapeCollection.js
|
||||
'ShapeCollectionModel': true,
|
||||
'ShapeCollectionController': true,
|
||||
'ShapeCollectionView': true,
|
||||
// from shapeCreator.js
|
||||
'ShapeCreatorModel': true,
|
||||
'ShapeCreatorController': true,
|
||||
'ShapeCreatorView': true,
|
||||
// from shapeGrouper.js
|
||||
'ShapeGrouperModel': true,
|
||||
'ShapeGrouperController': true,
|
||||
'ShapeGrouperView': true,
|
||||
// from labelsInfo.js
|
||||
'LabelsInfo': true,
|
||||
// from listener.js
|
||||
'Listener': true,
|
||||
// from logger.js
|
||||
'Logger': true,
|
||||
// from shapeMerger.js
|
||||
'ShapeMergerModel': true,
|
||||
'ShapeMergerController': true,
|
||||
'ShapeMergerView': true,
|
||||
// from shapes.js
|
||||
'PolyShapeModel': true,
|
||||
'PolyShapeView': true,
|
||||
'buildShapeModel': true,
|
||||
'buildShapeController': true,
|
||||
'buildShapeView': true,
|
||||
'STROKE_WIDTH': true,
|
||||
'POINT_RADIUS': true,
|
||||
'AREA_TRESHOLD': true,
|
||||
'SELECT_POINT_STROKE_WIDTH': true,
|
||||
// from mousetrap.js
|
||||
'Mousetrap': true,
|
||||
// from platform.js
|
||||
'platform': true,
|
||||
// from player.js
|
||||
'PlayerController': true,
|
||||
'PlayerModel': true,
|
||||
'PlayerView': true,
|
||||
// from server.js
|
||||
'serverRequest': true,
|
||||
'saveJobRequest': true,
|
||||
// from shapeBuffer.js
|
||||
'ShapeBufferController': true,
|
||||
'ShapeBufferModel': true,
|
||||
'ShapeBufferView': true,
|
||||
// from trackFilter.js
|
||||
'FilterModel': true,
|
||||
'FilterController': true,
|
||||
'FilterView': true,
|
||||
// from shapeSplitter.js
|
||||
'ShapeSplitter': true,
|
||||
// from userConfig.js
|
||||
'Config': true,
|
||||
// from cookies.js
|
||||
'Cookies': true,
|
||||
// from dashboard django template
|
||||
'maxUploadCount': true,
|
||||
'maxUploadSize': true,
|
||||
// from SVG.js
|
||||
'SVG': true,
|
||||
// from history.js
|
||||
'HistoryModel': true,
|
||||
'HistoryController': true,
|
||||
'HistoryView': true,
|
||||
// from polyshapeEditor.js
|
||||
'PolyshapeEditorModel': true,
|
||||
'PolyshapeEditorController': true,
|
||||
'PolyshapeEditorView': true,
|
||||
// from coordinateTranslator
|
||||
'CoordinateTranslator': true,
|
||||
},
|
||||
};
|
||||
@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
module.exports = function(config) {
|
||||
config.set({
|
||||
basePath: path.join(process.env.HOME, 'cvat/apps/'),
|
||||
frameworks: ['qunit'],
|
||||
files: [
|
||||
'engine/static/engine/js/labelsInfo.js',
|
||||
'engine/static/engine/js/annotationParser.js',
|
||||
'engine/static/engine/js/listener.js',
|
||||
'engine/static/engine/js/player.js',
|
||||
'engine/static/engine/js/shapes.js',
|
||||
'engine/static/engine/js/qunitTests.js',
|
||||
],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
autoWatch: false,
|
||||
browsers: ['ChromeNoSandbox'],
|
||||
|
||||
// Continuous Integration mode
|
||||
// if true, Karma captures browsers, runs the tests and exits
|
||||
singleRun: true,
|
||||
|
||||
// Concurrency level
|
||||
// how many browser should be started simultanous
|
||||
concurrency: Infinity,
|
||||
|
||||
preprocessors: {
|
||||
'**/!(qunitTests).js': ['coverage']
|
||||
},
|
||||
|
||||
reporters: ['progress', 'junit', 'coverage', 'coveralls'],
|
||||
|
||||
coverageReporter: {
|
||||
dir: path.join(process.env.HOME, 'media/coverage'),
|
||||
reporters: [
|
||||
{ type: 'html', subdir: '.' }, { type: 'lcov', subdir: '.' }
|
||||
],
|
||||
instrumenterOptions: {
|
||||
istanbul: { noCompact: true }
|
||||
}
|
||||
},
|
||||
|
||||
junitReporter: {
|
||||
outputDir: path.join(process.env.HOME, 'media/junit'),
|
||||
outputFile: undefined,
|
||||
useBrowserName: true,
|
||||
nameFormatter: undefined,
|
||||
classNameFormatter: undefined,
|
||||
properties: {},
|
||||
xmlVersion: null
|
||||
},
|
||||
|
||||
customLaunchers: {
|
||||
ChromeNoSandbox: {
|
||||
base: 'ChromeHeadless',
|
||||
flags: ['--no-sandbox']
|
||||
}
|
||||
},
|
||||
|
||||
logLevel: config.LOG_DEBUG
|
||||
});
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Copyright (C) 2018 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
#
|
||||
|
||||
eslint -c ~/tests/eslintrc.conf.js -f ~/tests/node_modules/eslint-detailed-reporter/lib/detailed-multi.js \
|
||||
~/cvat/apps/**/static/**/js/*.js -o ./report.html
|
||||
ret_code=$?
|
||||
|
||||
mkdir -p ~/media/eslint
|
||||
mv -t ~/media/eslint main.js styles.css report.html
|
||||
|
||||
exit $ret_code
|
||||