Release 0.5.0
commit
242b3dfebd
@ -1,2 +1,3 @@
|
||||
exclude_paths:
|
||||
- '**/3rdparty/**'
|
||||
- '**/engine/js/cvat-core.min.js'
|
||||
|
||||
@ -0,0 +1,420 @@
|
||||
[MASTER]
|
||||
|
||||
# Specify a configuration file.
|
||||
#rcfile=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
|
||||
# Add files or directories to the blacklist. They should be base names, not
|
||||
# paths.
|
||||
ignore=CVS
|
||||
|
||||
# Add files or directories matching the regex patterns to the blacklist. The
|
||||
# regex matches against base names, not paths.
|
||||
ignore-patterns=
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# List of plugins (as comma separated values of python modules names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=
|
||||
|
||||
# Use multiple processes to speed up Pylint.
|
||||
jobs=1
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code
|
||||
extension-pkg-whitelist=
|
||||
|
||||
# Allow optimization of some AST trees. This will activate a peephole AST
|
||||
# optimizer, which will apply various small optimizations. For instance, it can
|
||||
# be used to obtain the result of joining multiple strings with the addition
|
||||
# operator. Joining a lot of strings can lead to a maximum recursion error in
|
||||
# Pylint and this flag can prevent that. It has one side effect, the resulting
|
||||
# AST will be different than the one from reality. This option is deprecated
|
||||
# and it will be removed in Pylint 2.0.
|
||||
optimize-ast=no
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
|
||||
confidence=
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once). See also the "--disable" option for examples.
|
||||
disable=all
|
||||
enable= E0001,E0100,E0101,E0102,E0103,E0104,E0105,E0106,E0107,E0110,
|
||||
E0113,E0114,E0115,E0116,E0117,E0108,E0202,E0203,E0211,E0236,
|
||||
E0238,E0239,E0240,E0241,E0301,E0302,E0601,E0603,E0604,E0701,
|
||||
E0702,E0703,E0704,E0710,E0711,E0712,E1003,E1102,E1111,E0112,
|
||||
E1120,E1121,E1123,E1124,E1125,E1126,E1127,E1132,E1200,E1201,
|
||||
E1205,E1206,E1300,E1301,E1302,E1303,E1304,E1305,E1306,
|
||||
C0123,C0200,C0303,C1001,
|
||||
W0101,W0102,W0104,W0105,W0106,W0107,W0108,W0109,W0110,W0120,
|
||||
W0122,W0124,W0150,W0199,W0221,W0222,W0233,W0404,W0410,W0601,
|
||||
W0602,W0604,W0611,W0612,W0622,W0623,W0702,W0705,W0711,W1300,
|
||||
W1301,W1302,W1303,,W1305,W1306,W1307
|
||||
R0102,R0201,R0202,R0203
|
||||
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once).You can also use "--disable=all" to
|
||||
# disable everything first and then reenable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use"--disable=all --enable=classes
|
||||
# --disable=W"
|
||||
#disable=old-octal-literal,basestring-builtin,no-absolute-import,old-division,coerce-method,long-suffix,reload-builtin,unichr-builtin,indexing-exception,raising-string,dict-iter-method,metaclass-assignment,filter-builtin-not-iterating,import-star-module-level,next-method-called,cmp-method,raw_input-builtin,old-raise-syntax,cmp-builtin,apply-builtin,getslice-method,input-builtin,backtick,coerce-builtin,range-builtin-not-iterating,xrange-builtin,using-cmp-argument,buffer-builtin,hex-method,execfile-builtin,unpacking-in-except,standarderror-builtin,round-builtin,nonzero-method,unicode-builtin,reduce-builtin,file-builtin,dict-view-method,old-ne-operator,print-statement,suppressed-message,oct-method,useless-suppression,delslice-method,long-builtin,setslice-method,zip-builtin-not-iterating,map-builtin-not-iterating,intern-builtin,parameter-unpacking
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, msvs
|
||||
# (visual studio) and html. You can also give a reporter class, eg
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
output-format=text
|
||||
|
||||
# Put messages in a separate file for each module / package specified on the
|
||||
# command line instead of printing them on stdout. Reports (if any) will be
|
||||
# written in a file name "pylint_global.[txt|html]". This option is deprecated
|
||||
# and it will be removed in Pylint 2.0.
|
||||
files-output=no
|
||||
|
||||
# Tells whether to display a full report or only the messages
|
||||
reports=yes
|
||||
|
||||
# Python expression which should return a note less than 10 (10 is the highest
|
||||
# note). You have access to the variables errors warning, statement which
|
||||
# respectively contain the number of errors / warnings messages and the total
|
||||
# number of statements analyzed. This is used by the global evaluation report
|
||||
# (RP0004).
|
||||
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details
|
||||
#msg-template=
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma
|
||||
good-names=i,j,k,ex,Run,_
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma
|
||||
bad-names=foo,bar,baz,toto,tutu,tata
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name
|
||||
include-naming-hint=no
|
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||
# to this list to register other decorators that produce valid properties.
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Regular expression matching correct constant names
|
||||
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
|
||||
|
||||
# Naming hint for constant names
|
||||
const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
|
||||
|
||||
# Regular expression matching correct class names
|
||||
class-rgx=[A-Z_][a-zA-Z0-9]+$
|
||||
|
||||
# Naming hint for class names
|
||||
class-name-hint=[A-Z_][a-zA-Z0-9]+$
|
||||
|
||||
# Regular expression matching correct argument names
|
||||
argument-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Naming hint for argument names
|
||||
argument-name-hint=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression matching correct variable names
|
||||
variable-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Naming hint for variable names
|
||||
variable-name-hint=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression matching correct class attribute names
|
||||
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
|
||||
|
||||
# Naming hint for class attribute names
|
||||
class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
|
||||
|
||||
# Regular expression matching correct method names
|
||||
method-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Naming hint for method names
|
||||
method-name-hint=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression matching correct module names
|
||||
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
|
||||
|
||||
# Naming hint for module names
|
||||
module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
|
||||
|
||||
# Regular expression matching correct function names
|
||||
function-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Naming hint for function names
|
||||
function-name-hint=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression matching correct attribute names
|
||||
attr-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Naming hint for attribute names
|
||||
attr-name-hint=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression matching correct inline iteration names
|
||||
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
|
||||
|
||||
# Naming hint for inline iteration names
|
||||
inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=^_
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
|
||||
[ELIF]
|
||||
|
||||
# Maximum number of nested blocks for function / method body
|
||||
max-nested-blocks=5
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=100
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
# List of optional constructs for which whitespace checking is disabled. `dict-
|
||||
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
|
||||
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
|
||||
# `empty-line` allows space-only lines.
|
||||
no-space-check=trailing-comma,dict-separator
|
||||
|
||||
# Maximum number of lines in a module
|
||||
max-module-lines=1000
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
expected-line-ending-format=
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,XXX,TODO
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
# Ignore comments when computing similarities.
|
||||
ignore-comments=yes
|
||||
|
||||
# Ignore docstrings when computing similarities.
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Ignore imports when computing similarities.
|
||||
ignore-imports=no
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Spelling dictionary name. Available dictionaries: none. To make it working
|
||||
# install python-enchant package.
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to indicated private dictionary in
|
||||
# --spelling-private-dict-file option instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||
ignore-mixin-members=yes
|
||||
|
||||
# List of module names for which member attributes should not be checked
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis. It
|
||||
# supports qualified module names, as well as Unix pattern matching.
|
||||
ignored-modules=
|
||||
|
||||
# List of class names for which member attributes should not be checked (useful
|
||||
# for classes with dynamically set attributes). This supports the use of
|
||||
# qualified names.
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=
|
||||
|
||||
# List of decorators that produce context managers, such as
|
||||
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||
# produce valid context managers.
|
||||
contextmanager-decorators=contextlib.contextmanager
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expectedly
|
||||
# not used).
|
||||
dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid to define new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
# List of strings which can identify a callback function by name. A callback
|
||||
# name must start or end with one of those strings.
|
||||
callbacks=cb_,_cb
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
redefining-builtins-modules=six.moves,future.builtins
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,__new__,setUp
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=mcs
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=_asdict,_fields,_replace,_source,_make
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# Maximum number of arguments for function / method
|
||||
max-args=5
|
||||
|
||||
# Argument names that match this expression will be ignored. Default to name
|
||||
# with leading underscore
|
||||
ignored-argument-names=_.*
|
||||
|
||||
# Maximum number of locals for function / method body
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of return / yield for function / method body
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of branch for function / method body
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of statements in function / method body
|
||||
max-statements=50
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=7
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=7
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=2
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
# Maximum number of boolean expressions in a if statement
|
||||
max-bool-expr=5
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma
|
||||
deprecated-modules=optparse
|
||||
|
||||
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||
# given file (report RP0402 must not be disabled)
|
||||
import-graph=
|
||||
|
||||
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||
# not be disabled)
|
||||
ext-import-graph=
|
||||
|
||||
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||
# not be disabled)
|
||||
int-import-graph=
|
||||
|
||||
# Force import order to recognize a module as part of the standard
|
||||
# compatibility libraries.
|
||||
known-standard-library=
|
||||
|
||||
# Force import order to recognize a module as part of a third party library.
|
||||
known-third-party=enchant
|
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||
# 3 compatible code, which means that the block might have code that exists
|
||||
# only in one or another interpreter, leading to false positives when analysed.
|
||||
analyse-fallback-blocks=no
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when being caught. Defaults to
|
||||
# "Exception"
|
||||
overgeneral-exceptions=Exception
|
||||
@ -0,0 +1,23 @@
|
||||
{
|
||||
"python.pythonPath": ".env/bin/python",
|
||||
"eslint.enable": true,
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"typescript"
|
||||
],
|
||||
"eslint.workingDirectories": [
|
||||
{
|
||||
"directory": "./cvat-core",
|
||||
"changeProcessCWD": true
|
||||
},
|
||||
{
|
||||
"directory": "./cvat-canvas",
|
||||
"changeProcessCWD": true
|
||||
},
|
||||
{
|
||||
"directory": ".",
|
||||
"changeProcessCWD": true
|
||||
}
|
||||
],
|
||||
"python.linting.pylintEnabled": true
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "ui.js: server",
|
||||
"type": "npm",
|
||||
"script": "start",
|
||||
"path": "cvat-ui/",
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
'env': {
|
||||
'node': true,
|
||||
'browser': true,
|
||||
'es6': true,
|
||||
},
|
||||
'parserOptions': {
|
||||
'parser': '@typescript-eslint/parser',
|
||||
'ecmaVersion': 6,
|
||||
},
|
||||
'plugins': [
|
||||
'@typescript-eslint',
|
||||
'import',
|
||||
],
|
||||
'extends': [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'airbnb-typescript/base',
|
||||
'plugin:import/errors',
|
||||
'plugin:import/warnings',
|
||||
'plugin:import/typescript',
|
||||
],
|
||||
'rules': {
|
||||
'@typescript-eslint/no-explicit-any': 0,
|
||||
'@typescript-eslint/indent': ['warn', 4],
|
||||
'no-plusplus': 0,
|
||||
'no-restricted-syntax': [
|
||||
0,
|
||||
{
|
||||
'selector': 'ForOfStatement'
|
||||
}
|
||||
],
|
||||
'no-continue': 0,
|
||||
'func-names': 0,
|
||||
'no-console': 0, // this rule deprecates console.log, console.warn etc. because 'it is not good in production code'
|
||||
'lines-between-class-members': 0,
|
||||
'import/prefer-default-export': 0, // works incorrect with interfaces
|
||||
'newline-per-chained-call': 0, // makes code uglier
|
||||
},
|
||||
'settings': {
|
||||
'import/resolver': {
|
||||
'node': {
|
||||
'extensions': ['.ts', '.js', '.json'],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,2 @@
|
||||
src/*.js
|
||||
dist
|
||||
@ -0,0 +1,198 @@
|
||||
# Module CVAT-CANVAS
|
||||
|
||||
## Description
|
||||
The CVAT module written in TypeScript language.
|
||||
It presents a canvas to viewing, drawing and editing of annotations.
|
||||
|
||||
## Commands
|
||||
- Building of the module from sources in the ```dist``` directory:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm run build -- --mode=development # without a minification
|
||||
```
|
||||
|
||||
- Updating of a module version:
|
||||
```bash
|
||||
npm version patch # updated after minor fixes
|
||||
npm version minor # updated after major changes which don't affect API compatibility with previous versions
|
||||
npm version major # updated after major changes which affect API compatibility with previous versions
|
||||
```
|
||||
|
||||
## Using
|
||||
|
||||
Canvas itself handles:
|
||||
- Shape context menu (PKM)
|
||||
- Image moving (mousedrag)
|
||||
- Image resizing (mousewheel)
|
||||
- Image fit (dblclick)
|
||||
- Remove point (PKM)
|
||||
- Polyshape editing (Shift + LKM)
|
||||
|
||||
### API Methods
|
||||
|
||||
```ts
|
||||
enum Rotation {
|
||||
ANTICLOCKWISE90,
|
||||
CLOCKWISE90,
|
||||
}
|
||||
|
||||
interface DrawData {
|
||||
enabled: boolean;
|
||||
shapeType?: string;
|
||||
numberOfPoints?: number;
|
||||
initialState?: any;
|
||||
crosshair?: boolean;
|
||||
}
|
||||
|
||||
interface GroupData {
|
||||
enabled: boolean;
|
||||
resetGroup?: boolean;
|
||||
}
|
||||
|
||||
interface MergeData {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface SplitData {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface DrawnData {
|
||||
shapeType: string;
|
||||
points: number[];
|
||||
objectType?: string;
|
||||
occluded?: boolean;
|
||||
attributes?: [index: number]: string;
|
||||
label?: Label;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
interface Canvas {
|
||||
html(): HTMLDivElement;
|
||||
setup(frameData: any, objectStates: any[]): void;
|
||||
activate(clientID: number, attributeID?: number): void;
|
||||
rotate(rotation: Rotation, remember?: boolean): void;
|
||||
focus(clientID: number, padding?: number): void;
|
||||
fit(): void;
|
||||
grid(stepX: number, stepY: number): void;
|
||||
|
||||
draw(drawData: DrawData): void;
|
||||
group(groupData: GroupData): void;
|
||||
split(splitData: SplitData): void;
|
||||
merge(mergeData: MergeData): void;
|
||||
select(objectState: any): void;
|
||||
|
||||
cancel(): void;
|
||||
}
|
||||
```
|
||||
|
||||
### API CSS
|
||||
|
||||
- All drawn objects (shapes, tracks) have an id ```cvat_canvas_shape_{objectState.clientID}```
|
||||
- Drawn shapes and tracks have classes ```cvat_canvas_shape```,
|
||||
```cvat_canvas_shape_activated```,
|
||||
```cvat_canvas_shape_grouping```,
|
||||
```cvat_canvas_shape_merging```,
|
||||
```cvat_canvas_shape_drawing```,
|
||||
```cvat_canvas_shape_occluded```
|
||||
- Drawn texts have the class ```cvat_canvas_text```
|
||||
- Tags have the class ```cvat_canvas_tag```
|
||||
- Canvas image has ID ```cvat_canvas_image```
|
||||
- Grid on the canvas has ID ```cvat_canvas_grid_pattern```
|
||||
- Crosshair during a draw has class ```cvat_canvas_crosshair```
|
||||
|
||||
### Events
|
||||
|
||||
Standard JS events are used.
|
||||
```js
|
||||
- canvas.setup
|
||||
- canvas.activated => ObjectState
|
||||
- canvas.deactivated
|
||||
- canvas.moved => {states: ObjectState[], x: number, y: number}
|
||||
- canvas.find => {states: ObjectState[], x: number, y: number}
|
||||
- canvas.drawn => {state: DrawnData}
|
||||
- canvas.edited => {state: ObjectState, points: number[]}
|
||||
- canvas.splitted => {state: ObjectState}
|
||||
- canvas.groupped => {states: ObjectState[]}
|
||||
- canvas.merged => {states: ObjectState[]}
|
||||
- canvas.canceled
|
||||
```
|
||||
|
||||
### WEB
|
||||
```js
|
||||
// Create an instance of a canvas
|
||||
const canvas = new window.canvas.Canvas();
|
||||
|
||||
// Put canvas to a html container
|
||||
htmlContainer.appendChild(canvas.html());
|
||||
|
||||
// Next you can use its API methods. For example:
|
||||
canvas.rotate(window.Canvas.Rotation.CLOCKWISE90);
|
||||
canvas.draw({
|
||||
enabled: true,
|
||||
shapeType: 'rectangle',
|
||||
crosshair: true,
|
||||
});
|
||||
```
|
||||
|
||||
### TypeScript
|
||||
- Add to ```tsconfig.json```:
|
||||
```json
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"cvat-canvas.node": ["3rdparty/cvat-canvas.node"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- ```3rdparty``` directory contains both ```cvat-canvas.node.js``` and ```cvat-canvas.node.d.ts```.
|
||||
- Add alias to ```webpack.config.js```:
|
||||
```js
|
||||
module.exports = {
|
||||
resolve: {
|
||||
alias: {
|
||||
'cvat-canvas.node': path.resolve(__dirname, '3rdparty/cvat-canvas.node.js'),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Than you can use it in TypeScript:
|
||||
```ts
|
||||
import * as CANVAS from 'cvat-canvas.node';
|
||||
// Create an instance of a canvas
|
||||
const canvas = new CANVAS.Canvas();
|
||||
|
||||
// Put canvas to a html container
|
||||
htmlContainer.appendChild(canvas.html());
|
||||
|
||||
// Next you can use its API methods. For example:
|
||||
canvas.rotate(CANVAS.Rotation.CLOCKWISE90);
|
||||
canvas.draw({
|
||||
enabled: true,
|
||||
shapeType: 'rectangle',
|
||||
crosshair: true,
|
||||
});
|
||||
```
|
||||
|
||||
## States
|
||||
|
||||

|
||||
|
||||
## API Reaction
|
||||
|
||||
| | IDLE | GROUPING | SPLITTING | DRAWING | MERGING | EDITING |
|
||||
|------------|------|----------|-----------|---------|---------|---------|
|
||||
| html() | + | + | + | + | + | + |
|
||||
| setup() | + | + | + | + | + | - |
|
||||
| activate() | + | - | - | - | - | - |
|
||||
| rotate() | + | + | + | + | + | + |
|
||||
| focus() | + | + | + | + | + | + |
|
||||
| fit() | + | + | + | + | + | + |
|
||||
| grid() | + | + | + | + | + | + |
|
||||
| draw() | + | - | - | - | - | - |
|
||||
| split() | + | - | + | - | - | - |
|
||||
| group | + | + | - | - | - | - |
|
||||
| merge() | + | - | - | - | + | - |
|
||||
| cancel() | - | + | + | + | + | + |
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 303 KiB |
@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "cvat-canvas",
|
||||
"version": "0.1.0",
|
||||
"description": "Part of Computer Vision Annotation Tool which presents its canvas library",
|
||||
"main": "src/canvas.ts",
|
||||
"scripts": {
|
||||
"build": "tsc && webpack --config ./webpack.config.js",
|
||||
"server": "nodemon --watch config --exec 'webpack-dev-server --config ./webpack.config.js --mode=development --open'"
|
||||
},
|
||||
"author": "Intel",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"svg.draggable.js": "2.2.2",
|
||||
"svg.draw.js": "^2.0.3",
|
||||
"svg.js": "2.7.1",
|
||||
"svg.resize.js": "1.4.3",
|
||||
"svg.select.js": "3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.5.5",
|
||||
"@babel/core": "^7.5.5",
|
||||
"@babel/preset-env": "^7.5.5",
|
||||
"@babel/preset-typescript": "^7.3.3",
|
||||
"@types/node": "^12.6.8",
|
||||
"@typescript-eslint/eslint-plugin": "^1.13.0",
|
||||
"@typescript-eslint/parser": "^1.13.0",
|
||||
"babel-loader": "^8.0.6",
|
||||
"css-loader": "^3.2.0",
|
||||
"dts-bundle-webpack": "^1.0.2",
|
||||
"eslint": "^6.1.0",
|
||||
"eslint-config-airbnb-typescript": "^4.0.1",
|
||||
"eslint-config-typescript-recommended": "^1.4.17",
|
||||
"eslint-plugin-import": "^2.18.2",
|
||||
"nodemon": "^1.19.1",
|
||||
"style-loader": "^1.0.0",
|
||||
"typescript": "^3.5.3",
|
||||
"webpack": "^4.36.1",
|
||||
"webpack-cli": "^3.3.6",
|
||||
"webpack-dev-server": "^3.7.2"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,148 @@
|
||||
.cvat_canvas_hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cvat_canvas_shape {
|
||||
fill-opacity: 0.05;
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
|
||||
polyline.cvat_canvas_shape {
|
||||
fill-opacity: 0;
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
|
||||
.cvat_canvas_text {
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
fill: white;
|
||||
cursor: default;
|
||||
font-family: Calibri, Candara, Segoe, "Segoe UI", Optima, Arial, sans-serif;
|
||||
text-shadow: 0px 0px 4px black;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cvat_canvas_crosshair {
|
||||
stroke: red;
|
||||
}
|
||||
|
||||
.cvat_canvas_shape_activated {
|
||||
fill-opacity: 0.3;
|
||||
}
|
||||
|
||||
.cvat_canvas_shape_grouping {
|
||||
fill: darkmagenta;
|
||||
fill-opacity: 0.5;
|
||||
}
|
||||
|
||||
polyline.cvat_canvas_shape_grouping {
|
||||
stroke: darkmagenta;
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
|
||||
.cvat_canvas_shape_merging {
|
||||
fill: blue;
|
||||
fill-opacity: 0.5;
|
||||
}
|
||||
|
||||
polyline.cvat_canvas_shape_splitting {
|
||||
stroke: dodgerblue;
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
|
||||
.cvat_canvas_shape_splitting {
|
||||
fill: dodgerblue;
|
||||
fill-opacity: 0.5;
|
||||
}
|
||||
|
||||
polyline.cvat_canvas_shape_merging {
|
||||
stroke: blue;
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
|
||||
.cvat_canvas_shape_drawing {
|
||||
fill-opacity: 0.1;
|
||||
stroke-opacity: 1;
|
||||
fill: white;
|
||||
stroke: black;
|
||||
}
|
||||
|
||||
.cvat_canvas_shape_occluded {
|
||||
stroke-dasharray: 5;
|
||||
}
|
||||
|
||||
.svg_select_boundingRect {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#cvat_canvas_wrapper {
|
||||
width: 100%;
|
||||
height: 93%;
|
||||
border-radius: 5px;
|
||||
background-color: white;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#cvat_canvas_loading_animation {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#cvat_canvas_loading_circle {
|
||||
fill-opacity: 0;
|
||||
stroke: #09c;
|
||||
stroke-width: 3px;
|
||||
stroke-dasharray: 50;
|
||||
animation: loadingAnimation 1s linear infinite;
|
||||
}
|
||||
|
||||
#cvat_canvas_text_content {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#cvat_canvas_background {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
background-repeat: no-repeat;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.75);
|
||||
}
|
||||
|
||||
#cvat_canvas_grid {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#cvat_canvas_grid_pattern {
|
||||
opacity: 1;
|
||||
stroke: white;
|
||||
}
|
||||
|
||||
#cvat_canvas_content {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
outline: 10px solid black;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@keyframes loadingAnimation {
|
||||
0% {stroke-dashoffset: 1; stroke: #09c;}
|
||||
50% {stroke-dashoffset: 100; stroke: #f44;}
|
||||
100% {stroke-dashoffset: 300; stroke: #09c;}
|
||||
}
|
||||
@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import {
|
||||
Rotation,
|
||||
DrawData,
|
||||
MergeData,
|
||||
SplitData,
|
||||
GroupData,
|
||||
CanvasModel,
|
||||
CanvasModelImpl,
|
||||
} from './canvasModel';
|
||||
|
||||
import {
|
||||
Master,
|
||||
} from './master';
|
||||
|
||||
import {
|
||||
CanvasController,
|
||||
CanvasControllerImpl,
|
||||
} from './canvasController';
|
||||
|
||||
import {
|
||||
CanvasView,
|
||||
CanvasViewImpl,
|
||||
} from './canvasView';
|
||||
|
||||
|
||||
import '../css/canvas.css';
|
||||
|
||||
|
||||
interface Canvas {
|
||||
html(): HTMLDivElement;
|
||||
setup(frameData: any, objectStates: any[]): void;
|
||||
activate(clientID: number, attributeID?: number): void;
|
||||
rotate(rotation: Rotation, remember?: boolean): void;
|
||||
focus(clientID: number, padding?: number): void;
|
||||
fit(): void;
|
||||
grid(stepX: number, stepY: number): void;
|
||||
|
||||
draw(drawData: DrawData): void;
|
||||
group(groupData: GroupData): void;
|
||||
split(splitData: SplitData): void;
|
||||
merge(mergeData: MergeData): void;
|
||||
select(objectState: any): void;
|
||||
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
class CanvasImpl implements Canvas {
|
||||
private model: CanvasModel & Master;
|
||||
private controller: CanvasController;
|
||||
private view: CanvasView;
|
||||
|
||||
public constructor() {
|
||||
this.model = new CanvasModelImpl();
|
||||
this.controller = new CanvasControllerImpl(this.model);
|
||||
this.view = new CanvasViewImpl(this.model, this.controller);
|
||||
}
|
||||
|
||||
public html(): HTMLDivElement {
|
||||
return this.view.html();
|
||||
}
|
||||
|
||||
public setup(frameData: any, objectStates: any[]): void {
|
||||
this.model.setup(frameData, objectStates);
|
||||
}
|
||||
|
||||
public activate(clientID: number, attributeID: number = null): void {
|
||||
this.model.activate(clientID, attributeID);
|
||||
}
|
||||
|
||||
public rotate(rotation: Rotation, remember: boolean = false): void {
|
||||
this.model.rotate(rotation, remember);
|
||||
}
|
||||
|
||||
public focus(clientID: number, padding: number = 0): void {
|
||||
this.model.focus(clientID, padding);
|
||||
}
|
||||
|
||||
public fit(): void {
|
||||
this.model.fit();
|
||||
}
|
||||
|
||||
public grid(stepX: number, stepY: number): void {
|
||||
this.model.grid(stepX, stepY);
|
||||
}
|
||||
|
||||
public draw(drawData: DrawData): void {
|
||||
this.model.draw(drawData);
|
||||
}
|
||||
|
||||
public split(splitData: SplitData): void {
|
||||
this.model.split(splitData);
|
||||
}
|
||||
|
||||
public group(groupData: GroupData): void {
|
||||
this.model.group(groupData);
|
||||
}
|
||||
|
||||
public merge(mergeData: MergeData): void {
|
||||
this.model.merge(mergeData);
|
||||
}
|
||||
|
||||
public select(objectState: any): void {
|
||||
this.model.select(objectState);
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
this.model.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
CanvasImpl as Canvas,
|
||||
Rotation,
|
||||
};
|
||||
@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import {
|
||||
CanvasModel,
|
||||
Geometry,
|
||||
Position,
|
||||
FocusData,
|
||||
ActiveElement,
|
||||
DrawData,
|
||||
MergeData,
|
||||
SplitData,
|
||||
GroupData,
|
||||
Mode,
|
||||
} from './canvasModel';
|
||||
|
||||
export interface CanvasController {
|
||||
readonly objects: any[];
|
||||
readonly focusData: FocusData;
|
||||
readonly activeElement: ActiveElement;
|
||||
readonly drawData: DrawData;
|
||||
readonly mergeData: MergeData;
|
||||
readonly splitData: SplitData;
|
||||
readonly groupData: GroupData;
|
||||
readonly selected: any;
|
||||
mode: Mode;
|
||||
geometry: Geometry;
|
||||
|
||||
zoom(x: number, y: number, direction: number): void;
|
||||
draw(drawData: DrawData): void;
|
||||
merge(mergeData: MergeData): void;
|
||||
split(splitData: SplitData): void;
|
||||
group(groupData: GroupData): void;
|
||||
enableDrag(x: number, y: number): void;
|
||||
drag(x: number, y: number): void;
|
||||
disableDrag(): void;
|
||||
|
||||
fit(): void;
|
||||
}
|
||||
|
||||
export class CanvasControllerImpl implements CanvasController {
|
||||
private model: CanvasModel;
|
||||
private lastDragPosition: Position;
|
||||
private isDragging: boolean;
|
||||
|
||||
public constructor(model: CanvasModel) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public zoom(x: number, y: number, direction: number): void {
|
||||
this.model.zoom(x, y, direction);
|
||||
}
|
||||
|
||||
public fit(): void {
|
||||
this.model.fit();
|
||||
}
|
||||
|
||||
public enableDrag(x: number, y: number): void {
|
||||
this.lastDragPosition = {
|
||||
x,
|
||||
y,
|
||||
};
|
||||
this.isDragging = true;
|
||||
}
|
||||
|
||||
public drag(x: number, y: number): void {
|
||||
if (this.isDragging) {
|
||||
const topOffset: number = y - this.lastDragPosition.y;
|
||||
const leftOffset: number = x - this.lastDragPosition.x;
|
||||
this.lastDragPosition = {
|
||||
x,
|
||||
y,
|
||||
};
|
||||
this.model.move(topOffset, leftOffset);
|
||||
}
|
||||
}
|
||||
|
||||
public disableDrag(): void {
|
||||
this.isDragging = false;
|
||||
}
|
||||
|
||||
public draw(drawData: DrawData): void {
|
||||
this.model.draw(drawData);
|
||||
}
|
||||
|
||||
public merge(mergeData: MergeData): void {
|
||||
this.model.merge(mergeData);
|
||||
}
|
||||
|
||||
public split(splitData: SplitData): void {
|
||||
this.model.split(splitData);
|
||||
}
|
||||
|
||||
public group(groupData: GroupData): void {
|
||||
this.model.group(groupData);
|
||||
}
|
||||
|
||||
public get geometry(): Geometry {
|
||||
return this.model.geometry;
|
||||
}
|
||||
|
||||
public set geometry(geometry: Geometry) {
|
||||
this.model.geometry = geometry;
|
||||
}
|
||||
|
||||
public get objects(): any[] {
|
||||
return this.model.objects;
|
||||
}
|
||||
|
||||
public get focusData(): FocusData {
|
||||
return this.model.focusData;
|
||||
}
|
||||
|
||||
public get activeElement(): ActiveElement {
|
||||
return this.model.activeElement;
|
||||
}
|
||||
|
||||
public get drawData(): DrawData {
|
||||
return this.model.drawData;
|
||||
}
|
||||
|
||||
public get mergeData(): MergeData {
|
||||
return this.model.mergeData;
|
||||
}
|
||||
|
||||
public get splitData(): SplitData {
|
||||
return this.model.splitData;
|
||||
}
|
||||
|
||||
public get groupData(): GroupData {
|
||||
return this.model.groupData;
|
||||
}
|
||||
|
||||
public get selected(): any {
|
||||
return this.model.selected;
|
||||
}
|
||||
|
||||
public set mode(value: Mode) {
|
||||
this.model.mode = value;
|
||||
}
|
||||
|
||||
public get mode(): Mode {
|
||||
return this.model.mode;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,504 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
// Disable till full implementation
|
||||
/* eslint class-methods-use-this: "off" */
|
||||
|
||||
import { MasterImpl } from './master';
|
||||
|
||||
|
||||
export interface Size {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Geometry {
|
||||
image: Size;
|
||||
canvas: Size;
|
||||
grid: Size;
|
||||
top: number;
|
||||
left: number;
|
||||
scale: number;
|
||||
offset: number;
|
||||
angle: number;
|
||||
}
|
||||
|
||||
export interface FocusData {
|
||||
clientID: number;
|
||||
padding: number;
|
||||
}
|
||||
|
||||
export interface ActiveElement {
|
||||
clientID: number;
|
||||
attributeID: number;
|
||||
}
|
||||
|
||||
export interface DrawData {
|
||||
enabled: boolean;
|
||||
shapeType?: string;
|
||||
numberOfPoints?: number;
|
||||
initialState?: any;
|
||||
crosshair?: boolean;
|
||||
}
|
||||
|
||||
export interface EditData {
|
||||
enabled: boolean;
|
||||
state: any;
|
||||
pointID: number;
|
||||
}
|
||||
|
||||
export interface GroupData {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface MergeData {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface SplitData {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export enum FrameZoom {
|
||||
MIN = 0.1,
|
||||
MAX = 10,
|
||||
}
|
||||
|
||||
export enum Rotation {
|
||||
ANTICLOCKWISE90,
|
||||
CLOCKWISE90,
|
||||
}
|
||||
|
||||
export enum UpdateReasons {
|
||||
IMAGE = 'image',
|
||||
OBJECTS = 'objects',
|
||||
ZOOM = 'zoom',
|
||||
FIT = 'fit',
|
||||
MOVE = 'move',
|
||||
GRID = 'grid',
|
||||
FOCUS = 'focus',
|
||||
ACTIVATE = 'activate',
|
||||
DRAW = 'draw',
|
||||
MERGE = 'merge',
|
||||
SPLIT = 'split',
|
||||
GROUP = 'group',
|
||||
SELECT = 'select',
|
||||
CANCEL = 'cancel',
|
||||
}
|
||||
|
||||
export enum Mode {
|
||||
IDLE = 'idle',
|
||||
DRAG = 'drag',
|
||||
RESIZE = 'resize',
|
||||
DRAW = 'draw',
|
||||
EDIT = 'edit',
|
||||
MERGE = 'merge',
|
||||
SPLIT = 'split',
|
||||
GROUP = 'group',
|
||||
}
|
||||
|
||||
export interface CanvasModel {
|
||||
readonly image: string;
|
||||
readonly objects: any[];
|
||||
readonly gridSize: Size;
|
||||
readonly focusData: FocusData;
|
||||
readonly activeElement: ActiveElement;
|
||||
readonly drawData: DrawData;
|
||||
readonly mergeData: MergeData;
|
||||
readonly splitData: SplitData;
|
||||
readonly groupData: GroupData;
|
||||
readonly selected: any;
|
||||
geometry: Geometry;
|
||||
mode: Mode;
|
||||
|
||||
zoom(x: number, y: number, direction: number): void;
|
||||
move(topOffset: number, leftOffset: number): void;
|
||||
|
||||
setup(frameData: any, objectStates: any[]): void;
|
||||
activate(clientID: number, attributeID: number): void;
|
||||
rotate(rotation: Rotation, remember: boolean): void;
|
||||
focus(clientID: number, padding: number): void;
|
||||
fit(): void;
|
||||
grid(stepX: number, stepY: number): void;
|
||||
|
||||
draw(drawData: DrawData): void;
|
||||
group(groupData: GroupData): void;
|
||||
split(splitData: SplitData): void;
|
||||
merge(mergeData: MergeData): void;
|
||||
select(objectState: any): void;
|
||||
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
export class CanvasModelImpl extends MasterImpl implements CanvasModel {
|
||||
private data: {
|
||||
activeElement: ActiveElement;
|
||||
angle: number;
|
||||
canvasSize: Size;
|
||||
image: string;
|
||||
imageOffset: number;
|
||||
imageSize: Size;
|
||||
focusData: FocusData;
|
||||
gridSize: Size;
|
||||
left: number;
|
||||
objects: any[];
|
||||
rememberAngle: boolean;
|
||||
scale: number;
|
||||
top: number;
|
||||
drawData: DrawData;
|
||||
mergeData: MergeData;
|
||||
groupData: GroupData;
|
||||
splitData: SplitData;
|
||||
selected: any;
|
||||
mode: Mode;
|
||||
};
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
|
||||
this.data = {
|
||||
activeElement: {
|
||||
clientID: null,
|
||||
attributeID: null,
|
||||
},
|
||||
angle: 0,
|
||||
canvasSize: {
|
||||
height: 0,
|
||||
width: 0,
|
||||
},
|
||||
image: '',
|
||||
imageOffset: 0,
|
||||
imageSize: {
|
||||
height: 0,
|
||||
width: 0,
|
||||
},
|
||||
focusData: {
|
||||
clientID: 0,
|
||||
padding: 0,
|
||||
},
|
||||
gridSize: {
|
||||
height: 100,
|
||||
width: 100,
|
||||
},
|
||||
left: 0,
|
||||
objects: [],
|
||||
rememberAngle: false,
|
||||
scale: 1,
|
||||
top: 0,
|
||||
drawData: {
|
||||
enabled: false,
|
||||
shapeType: null,
|
||||
numberOfPoints: null,
|
||||
initialState: null,
|
||||
},
|
||||
mergeData: {
|
||||
enabled: false,
|
||||
},
|
||||
groupData: {
|
||||
enabled: false,
|
||||
},
|
||||
splitData: {
|
||||
enabled: false,
|
||||
},
|
||||
selected: null,
|
||||
mode: null,
|
||||
};
|
||||
}
|
||||
|
||||
public zoom(x: number, y: number, direction: number): void {
|
||||
const oldScale: number = this.data.scale;
|
||||
const newScale: number = direction > 0 ? oldScale * 6 / 5 : oldScale * 5 / 6;
|
||||
this.data.scale = Math.min(Math.max(newScale, FrameZoom.MIN), FrameZoom.MAX);
|
||||
|
||||
const { angle } = this.data;
|
||||
|
||||
const mutiplier = Math.sin(angle * Math.PI / 180) + Math.cos(angle * Math.PI / 180);
|
||||
if ((angle / 90) % 2) {
|
||||
// 90, 270, ..
|
||||
this.data.top += mutiplier * ((x - this.data.imageSize.width / 2)
|
||||
* (oldScale / this.data.scale - 1)) * this.data.scale;
|
||||
this.data.left -= mutiplier * ((y - this.data.imageSize.height / 2)
|
||||
* (oldScale / this.data.scale - 1)) * this.data.scale;
|
||||
} else {
|
||||
this.data.left += mutiplier * ((x - this.data.imageSize.width / 2)
|
||||
* (oldScale / this.data.scale - 1)) * this.data.scale;
|
||||
this.data.top += mutiplier * ((y - this.data.imageSize.height / 2)
|
||||
* (oldScale / this.data.scale - 1)) * this.data.scale;
|
||||
}
|
||||
|
||||
this.notify(UpdateReasons.ZOOM);
|
||||
}
|
||||
|
||||
public move(topOffset: number, leftOffset: number): void {
|
||||
this.data.top += topOffset;
|
||||
this.data.left += leftOffset;
|
||||
this.notify(UpdateReasons.MOVE);
|
||||
}
|
||||
|
||||
public setup(frameData: any, objectStates: any[]): void {
|
||||
frameData.data(
|
||||
(): void => {
|
||||
this.data.image = '';
|
||||
this.notify(UpdateReasons.IMAGE);
|
||||
},
|
||||
).then((data: string): void => {
|
||||
this.data.imageSize = {
|
||||
height: (frameData.height as number),
|
||||
width: (frameData.width as number),
|
||||
};
|
||||
|
||||
if (!this.data.rememberAngle) {
|
||||
this.data.angle = 0;
|
||||
}
|
||||
|
||||
this.data.image = data;
|
||||
this.notify(UpdateReasons.IMAGE);
|
||||
this.data.objects = objectStates;
|
||||
this.notify(UpdateReasons.OBJECTS);
|
||||
}).catch((exception: any): void => {
|
||||
throw exception;
|
||||
});
|
||||
}
|
||||
|
||||
public activate(clientID: number, attributeID: number): void {
|
||||
if (this.data.mode !== Mode.IDLE) {
|
||||
// Exception or just return?
|
||||
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
|
||||
}
|
||||
|
||||
this.data.activeElement = {
|
||||
clientID,
|
||||
attributeID,
|
||||
};
|
||||
|
||||
this.notify(UpdateReasons.ACTIVATE);
|
||||
}
|
||||
|
||||
public rotate(rotation: Rotation, remember: boolean = false): void {
|
||||
if (rotation === Rotation.CLOCKWISE90) {
|
||||
this.data.angle += 90;
|
||||
} else {
|
||||
this.data.angle -= 90;
|
||||
}
|
||||
|
||||
this.data.angle %= 360;
|
||||
this.data.rememberAngle = remember;
|
||||
this.fit();
|
||||
}
|
||||
|
||||
public focus(clientID: number, padding: number): void {
|
||||
this.data.focusData = {
|
||||
clientID,
|
||||
padding,
|
||||
};
|
||||
|
||||
this.notify(UpdateReasons.FOCUS);
|
||||
}
|
||||
|
||||
public fit(): void {
|
||||
const { angle } = this.data;
|
||||
|
||||
if ((angle / 90) % 2) {
|
||||
// 90, 270, ..
|
||||
this.data.scale = Math.min(
|
||||
this.data.canvasSize.width / this.data.imageSize.height,
|
||||
this.data.canvasSize.height / this.data.imageSize.width,
|
||||
);
|
||||
} else {
|
||||
this.data.scale = Math.min(
|
||||
this.data.canvasSize.width / this.data.imageSize.width,
|
||||
this.data.canvasSize.height / this.data.imageSize.height,
|
||||
);
|
||||
}
|
||||
|
||||
this.data.scale = Math.min(
|
||||
Math.max(this.data.scale, FrameZoom.MIN),
|
||||
FrameZoom.MAX,
|
||||
);
|
||||
|
||||
this.data.top = (this.data.canvasSize.height / 2 - this.data.imageSize.height / 2);
|
||||
this.data.left = (this.data.canvasSize.width / 2 - this.data.imageSize.width / 2);
|
||||
|
||||
this.notify(UpdateReasons.FIT);
|
||||
}
|
||||
|
||||
public grid(stepX: number, stepY: number): void {
|
||||
this.data.gridSize = {
|
||||
height: stepY,
|
||||
width: stepX,
|
||||
};
|
||||
|
||||
this.notify(UpdateReasons.GRID);
|
||||
}
|
||||
|
||||
public draw(drawData: DrawData): void {
|
||||
if (![Mode.IDLE, Mode.DRAW].includes(this.data.mode)) {
|
||||
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
|
||||
}
|
||||
|
||||
if (drawData.enabled) {
|
||||
if (this.data.drawData.enabled) {
|
||||
throw new Error('Drawing has been already started');
|
||||
} else if (!drawData.shapeType && !drawData.initialState) {
|
||||
throw new Error('A shape type is not specified');
|
||||
} else if (typeof (drawData.numberOfPoints) !== 'undefined') {
|
||||
if (drawData.shapeType === 'polygon' && drawData.numberOfPoints < 3) {
|
||||
throw new Error('A polygon consists of at least 3 points');
|
||||
} else if (drawData.shapeType === 'polyline' && drawData.numberOfPoints < 2) {
|
||||
throw new Error('A polyline consists of at least 2 points');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.data.drawData = { ...drawData };
|
||||
if (this.data.drawData.initialState) {
|
||||
this.data.drawData.shapeType = this.data.drawData.initialState.shapeType;
|
||||
}
|
||||
this.notify(UpdateReasons.DRAW);
|
||||
}
|
||||
|
||||
public split(splitData: SplitData): void {
|
||||
if (![Mode.IDLE, Mode.SPLIT].includes(this.data.mode)) {
|
||||
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
|
||||
}
|
||||
|
||||
if (this.data.splitData.enabled && splitData.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.data.splitData.enabled && !splitData.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.data.splitData = { ...splitData };
|
||||
this.notify(UpdateReasons.SPLIT);
|
||||
}
|
||||
|
||||
public group(groupData: GroupData): void {
|
||||
if (![Mode.IDLE, Mode.GROUP].includes(this.data.mode)) {
|
||||
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
|
||||
}
|
||||
|
||||
if (this.data.groupData.enabled && groupData.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.data.groupData.enabled && !groupData.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.data.groupData = { ...groupData };
|
||||
this.notify(UpdateReasons.GROUP);
|
||||
}
|
||||
|
||||
public merge(mergeData: MergeData): void {
|
||||
if (![Mode.IDLE, Mode.MERGE].includes(this.data.mode)) {
|
||||
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
|
||||
}
|
||||
|
||||
if (this.data.mergeData.enabled && mergeData.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.data.mergeData.enabled && !mergeData.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.data.mergeData = { ...mergeData };
|
||||
this.notify(UpdateReasons.MERGE);
|
||||
}
|
||||
|
||||
public select(objectState: any): void {
|
||||
this.data.selected = objectState;
|
||||
this.notify(UpdateReasons.SELECT);
|
||||
this.data.selected = null;
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
this.notify(UpdateReasons.CANCEL);
|
||||
}
|
||||
|
||||
public get geometry(): Geometry {
|
||||
return {
|
||||
angle: this.data.angle,
|
||||
canvas: { ...this.data.canvasSize },
|
||||
image: { ...this.data.imageSize },
|
||||
grid: { ...this.data.gridSize },
|
||||
left: this.data.left,
|
||||
offset: this.data.imageOffset,
|
||||
scale: this.data.scale,
|
||||
top: this.data.top,
|
||||
};
|
||||
}
|
||||
|
||||
public set geometry(geometry: Geometry) {
|
||||
this.data.angle = geometry.angle;
|
||||
this.data.canvasSize = { ...geometry.canvas };
|
||||
this.data.imageSize = { ...geometry.image };
|
||||
this.data.gridSize = { ...geometry.grid };
|
||||
this.data.left = geometry.left;
|
||||
this.data.top = geometry.top;
|
||||
this.data.imageOffset = geometry.offset;
|
||||
this.data.scale = geometry.scale;
|
||||
|
||||
this.data.imageOffset = Math.floor(Math.max(
|
||||
this.data.canvasSize.height / FrameZoom.MIN,
|
||||
this.data.canvasSize.width / FrameZoom.MIN,
|
||||
));
|
||||
}
|
||||
|
||||
public get image(): string {
|
||||
return this.data.image;
|
||||
}
|
||||
|
||||
public get objects(): any[] {
|
||||
return this.data.objects;
|
||||
}
|
||||
|
||||
public get gridSize(): Size {
|
||||
return { ...this.data.gridSize };
|
||||
}
|
||||
|
||||
public get focusData(): FocusData {
|
||||
return { ...this.data.focusData };
|
||||
}
|
||||
|
||||
public get activeElement(): ActiveElement {
|
||||
return { ...this.data.activeElement };
|
||||
}
|
||||
|
||||
public get drawData(): DrawData {
|
||||
return { ...this.data.drawData };
|
||||
}
|
||||
|
||||
public get mergeData(): MergeData {
|
||||
return { ...this.data.mergeData };
|
||||
}
|
||||
|
||||
public get splitData(): SplitData {
|
||||
return { ...this.data.splitData };
|
||||
}
|
||||
|
||||
public get groupData(): GroupData {
|
||||
return { ...this.data.groupData };
|
||||
}
|
||||
|
||||
public get selected(): any {
|
||||
return this.data.selected;
|
||||
}
|
||||
|
||||
public set mode(value: Mode) {
|
||||
this.data.mode = value;
|
||||
}
|
||||
|
||||
public get mode(): Mode {
|
||||
return this.data.mode;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
const BASE_STROKE_WIDTH = 2;
|
||||
const BASE_GRID_WIDTH = 1;
|
||||
const BASE_POINT_SIZE = 5;
|
||||
const TEXT_MARGIN = 10;
|
||||
const AREA_THRESHOLD = 9;
|
||||
const SIZE_THRESHOLD = 3;
|
||||
const POINTS_STROKE_WIDTH = 1.5;
|
||||
const POINTS_SELECTED_STROKE_WIDTH = 4;
|
||||
|
||||
export default {
|
||||
BASE_STROKE_WIDTH,
|
||||
BASE_GRID_WIDTH,
|
||||
BASE_POINT_SIZE,
|
||||
TEXT_MARGIN,
|
||||
AREA_THRESHOLD,
|
||||
SIZE_THRESHOLD,
|
||||
POINTS_STROKE_WIDTH,
|
||||
POINTS_SELECTED_STROKE_WIDTH,
|
||||
};
|
||||
@ -0,0 +1,568 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import * as SVG from 'svg.js';
|
||||
import consts from './consts';
|
||||
import 'svg.draw.js';
|
||||
import './svg.patch';
|
||||
|
||||
import {
|
||||
DrawData,
|
||||
Geometry,
|
||||
} from './canvasModel';
|
||||
|
||||
import {
|
||||
translateToSVG,
|
||||
translateBetweenSVG,
|
||||
displayShapeSize,
|
||||
ShapeSizeElement,
|
||||
pointsToString,
|
||||
pointsToArray,
|
||||
BBox,
|
||||
Box,
|
||||
} from './shared';
|
||||
|
||||
export interface DrawHandler {
|
||||
draw(drawData: DrawData, geometry: Geometry): void;
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
export class DrawHandlerImpl implements DrawHandler {
|
||||
// callback is used to notify about creating new shape
|
||||
private onDrawDone: (data: object) => void;
|
||||
private canvas: SVG.Container;
|
||||
private text: SVG.Container;
|
||||
private background: SVGSVGElement;
|
||||
private crosshair: {
|
||||
x: SVG.Line;
|
||||
y: SVG.Line;
|
||||
};
|
||||
private drawData: DrawData;
|
||||
private geometry: Geometry;
|
||||
|
||||
// we should use any instead of SVG.Shape because svg plugins cannot change declared interface
|
||||
// so, methods like draw() just undefined for SVG.Shape, but nevertheless they exist
|
||||
private drawInstance: any;
|
||||
private shapeSizeElement: ShapeSizeElement;
|
||||
|
||||
private getFinalRectCoordinates(bbox: BBox): number[] {
|
||||
const frameWidth = this.geometry.image.width;
|
||||
const frameHeight = this.geometry.image.height;
|
||||
|
||||
let [xtl, ytl, xbr, ybr] = translateBetweenSVG(
|
||||
this.canvas.node as any as SVGSVGElement,
|
||||
this.background,
|
||||
[bbox.x, bbox.y, bbox.x + bbox.width, bbox.y + bbox.height],
|
||||
);
|
||||
|
||||
xtl = Math.min(Math.max(xtl, 0), frameWidth);
|
||||
xbr = Math.min(Math.max(xbr, 0), frameWidth);
|
||||
ytl = Math.min(Math.max(ytl, 0), frameHeight);
|
||||
ybr = Math.min(Math.max(ybr, 0), frameHeight);
|
||||
|
||||
return [xtl, ytl, xbr, ybr];
|
||||
}
|
||||
|
||||
private getFinalPolyshapeCoordinates(targetPoints: number[]): {
|
||||
points: number[];
|
||||
box: Box;
|
||||
} {
|
||||
const points = translateBetweenSVG(
|
||||
this.canvas.node as any as SVGSVGElement,
|
||||
this.background,
|
||||
targetPoints,
|
||||
);
|
||||
|
||||
const box = {
|
||||
xtl: Number.MAX_SAFE_INTEGER,
|
||||
ytl: Number.MAX_SAFE_INTEGER,
|
||||
xbr: Number.MAX_SAFE_INTEGER,
|
||||
ybr: Number.MAX_SAFE_INTEGER,
|
||||
};
|
||||
|
||||
const frameWidth = this.geometry.image.width;
|
||||
const frameHeight = this.geometry.image.height;
|
||||
for (let i = 0; i < points.length - 1; i += 2) {
|
||||
points[i] = Math.min(Math.max(points[i], 0), frameWidth);
|
||||
points[i + 1] = Math.min(Math.max(points[i + 1], 0), frameHeight);
|
||||
|
||||
box.xtl = Math.min(box.xtl, points[i]);
|
||||
box.ytl = Math.min(box.ytl, points[i + 1]);
|
||||
box.xbr = Math.max(box.xbr, points[i]);
|
||||
box.ybr = Math.max(box.ybr, points[i + 1]);
|
||||
}
|
||||
|
||||
return {
|
||||
points,
|
||||
box,
|
||||
};
|
||||
}
|
||||
|
||||
private addCrosshair(): void {
|
||||
this.crosshair = {
|
||||
x: this.canvas.line(0, 0, this.canvas.node.clientWidth, 0).attr({
|
||||
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * this.geometry.scale),
|
||||
zOrder: Number.MAX_SAFE_INTEGER,
|
||||
}).addClass('cvat_canvas_crosshair'),
|
||||
y: this.canvas.line(0, 0, 0, this.canvas.node.clientHeight).attr({
|
||||
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * this.geometry.scale),
|
||||
zOrder: Number.MAX_SAFE_INTEGER,
|
||||
}).addClass('cvat_canvas_crosshair'),
|
||||
};
|
||||
}
|
||||
|
||||
private removeCrosshair(): void {
|
||||
this.crosshair.x.remove();
|
||||
this.crosshair.y.remove();
|
||||
this.crosshair = null;
|
||||
}
|
||||
|
||||
private release(): void {
|
||||
this.canvas.off('mousedown.draw');
|
||||
this.canvas.off('mousemove.draw');
|
||||
this.canvas.off('click.draw');
|
||||
|
||||
if (this.drawInstance) {
|
||||
// Draw plugin isn't activated when draw from initialState
|
||||
// So, we don't need to use any draw events
|
||||
if (!this.drawData.initialState) {
|
||||
this.drawInstance.off('drawdone');
|
||||
this.drawInstance.off('drawstop');
|
||||
this.drawInstance.draw('stop');
|
||||
}
|
||||
|
||||
this.drawInstance.remove();
|
||||
this.drawInstance = null;
|
||||
}
|
||||
|
||||
if (this.shapeSizeElement) {
|
||||
this.shapeSizeElement.rm();
|
||||
this.shapeSizeElement = null;
|
||||
}
|
||||
|
||||
if (this.crosshair) {
|
||||
this.removeCrosshair();
|
||||
}
|
||||
}
|
||||
|
||||
private initDrawing(): void {
|
||||
if (this.drawData.crosshair) {
|
||||
this.addCrosshair();
|
||||
}
|
||||
}
|
||||
|
||||
private closeDrawing(): void {
|
||||
if (this.drawInstance) {
|
||||
// Draw plugin isn't activated when draw from initialState
|
||||
// So, we don't need to use any draw events
|
||||
if (!this.drawData.initialState) {
|
||||
const { drawInstance } = this;
|
||||
this.drawInstance = null;
|
||||
if (this.drawData.shapeType === 'rectangle') {
|
||||
drawInstance.draw('cancel');
|
||||
} else {
|
||||
drawInstance.draw('done');
|
||||
}
|
||||
this.drawInstance = drawInstance;
|
||||
this.release();
|
||||
} else {
|
||||
this.release();
|
||||
this.onDrawDone(null);
|
||||
}
|
||||
|
||||
// here is a cycle
|
||||
// onDrawDone => controller => model => view => closeDrawing
|
||||
// one call of closeDrawing is unuseful, but it's okey
|
||||
}
|
||||
}
|
||||
|
||||
private drawBox(): void {
|
||||
this.drawInstance = this.canvas.rect();
|
||||
this.drawInstance.draw({
|
||||
snapToGrid: 0.1,
|
||||
}).addClass('cvat_canvas_shape_drawing').attr({
|
||||
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
|
||||
z_order: Number.MAX_SAFE_INTEGER,
|
||||
}).on('drawupdate', (): void => {
|
||||
this.shapeSizeElement.update(this.drawInstance);
|
||||
}).on('drawstop', (e: Event): void => {
|
||||
const bbox = (e.target as SVGRectElement).getBBox();
|
||||
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
|
||||
|
||||
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
|
||||
this.onDrawDone({
|
||||
shapeType: this.drawData.shapeType,
|
||||
points: [xtl, ytl, xbr, ybr],
|
||||
});
|
||||
} else {
|
||||
this.onDrawDone(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private drawPolyshape(): void {
|
||||
this.drawInstance.attr({
|
||||
z_order: Number.MAX_SAFE_INTEGER,
|
||||
});
|
||||
|
||||
let size = this.drawData.numberOfPoints;
|
||||
const sizeDecrement = function sizeDecrement(): void {
|
||||
if (!--size) {
|
||||
this.drawInstance.draw('done');
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
const sizeIncrement = function sizeIncrement(): void {
|
||||
size++;
|
||||
};
|
||||
|
||||
if (this.drawData.numberOfPoints) {
|
||||
this.drawInstance.on('drawstart', sizeDecrement);
|
||||
this.drawInstance.on('drawpoint', sizeDecrement);
|
||||
this.drawInstance.on('undopoint', sizeIncrement);
|
||||
}
|
||||
|
||||
// Add ability to cancel the latest drawn point
|
||||
const handleUndo = function handleUndo(e: MouseEvent): void {
|
||||
if (e.which === 3) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.drawInstance.draw('undo');
|
||||
}
|
||||
}.bind(this);
|
||||
this.canvas.on('mousedown.draw', handleUndo);
|
||||
|
||||
// Add ability to draw shapes by sliding
|
||||
// We need to remember last drawn point
|
||||
// to implementation of slide drawing
|
||||
const lastDrawnPoint: {
|
||||
x: number;
|
||||
y: number;
|
||||
} = {
|
||||
x: null,
|
||||
y: null,
|
||||
};
|
||||
|
||||
const handleSlide = function handleSlide(e: MouseEvent): void {
|
||||
// TODO: Use enumeration after typification cvat-core
|
||||
if (e.shiftKey && ['polygon', 'polyline'].includes(this.drawData.shapeType)) {
|
||||
if (lastDrawnPoint.x === null || lastDrawnPoint.y === null) {
|
||||
this.drawInstance.draw('point', e);
|
||||
} else {
|
||||
const deltaTreshold = 15;
|
||||
const delta = Math.sqrt(
|
||||
((e.clientX - lastDrawnPoint.x) ** 2)
|
||||
+ ((e.clientY - lastDrawnPoint.y) ** 2),
|
||||
);
|
||||
if (delta > deltaTreshold) {
|
||||
this.drawInstance.draw('point', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}.bind(this);
|
||||
this.canvas.on('mousemove.draw', handleSlide);
|
||||
|
||||
// We need scale just drawn points
|
||||
const self = this;
|
||||
this.drawInstance.on('drawstart drawpoint', (e: CustomEvent): void => {
|
||||
self.transform(self.geometry);
|
||||
lastDrawnPoint.x = e.detail.event.clientX;
|
||||
lastDrawnPoint.y = e.detail.event.clientY;
|
||||
});
|
||||
|
||||
this.drawInstance.on('drawdone', (e: CustomEvent): void => {
|
||||
const targetPoints = pointsToArray((e.target as SVGElement).getAttribute('points'));
|
||||
|
||||
const {
|
||||
points,
|
||||
box,
|
||||
} = this.getFinalPolyshapeCoordinates(targetPoints);
|
||||
|
||||
if (this.drawData.shapeType === 'polygon'
|
||||
&& ((box.xbr - box.xtl) * (box.ybr - box.ytl) >= consts.AREA_THRESHOLD)
|
||||
&& points.length >= 3 * 2) {
|
||||
this.onDrawDone({
|
||||
shapeType: this.drawData.shapeType,
|
||||
points,
|
||||
});
|
||||
} else if (this.drawData.shapeType === 'polyline'
|
||||
&& ((box.xbr - box.xtl) >= consts.SIZE_THRESHOLD
|
||||
|| (box.ybr - box.ytl) >= consts.SIZE_THRESHOLD)
|
||||
&& points.length >= 2 * 2) {
|
||||
this.onDrawDone({
|
||||
shapeType: this.drawData.shapeType,
|
||||
points,
|
||||
});
|
||||
} else if (this.drawData.shapeType === 'points'
|
||||
&& (e.target as any).getAttribute('points') !== '0,0') {
|
||||
this.onDrawDone({
|
||||
shapeType: this.drawData.shapeType,
|
||||
points,
|
||||
});
|
||||
} else {
|
||||
this.onDrawDone(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private drawPolygon(): void {
|
||||
this.drawInstance = (this.canvas as any).polygon().draw({
|
||||
snapToGrid: 0.1,
|
||||
}).addClass('cvat_canvas_shape_drawing').style({
|
||||
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
|
||||
});
|
||||
|
||||
this.drawPolyshape();
|
||||
}
|
||||
|
||||
private drawPolyline(): void {
|
||||
this.drawInstance = (this.canvas as any).polyline().draw({
|
||||
snapToGrid: 0.1,
|
||||
}).addClass('cvat_canvas_shape_drawing').style({
|
||||
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
|
||||
'fill-opacity': 0,
|
||||
});
|
||||
|
||||
this.drawPolyshape();
|
||||
}
|
||||
|
||||
private drawPoints(): void {
|
||||
this.drawInstance = (this.canvas as any).polygon().draw({
|
||||
snapToGrid: 0.1,
|
||||
}).addClass('cvat_canvas_shape_drawing').style({
|
||||
'stroke-width': 0,
|
||||
opacity: 0,
|
||||
});
|
||||
|
||||
this.drawPolyshape();
|
||||
}
|
||||
|
||||
private pastePolyshape(): void {
|
||||
this.canvas.on('click.draw', (e: MouseEvent): void => {
|
||||
const targetPoints = (e.target as SVGElement)
|
||||
.getAttribute('points')
|
||||
.split(/[,\s]/g)
|
||||
.map((coord): number => +coord);
|
||||
|
||||
const { points } = this.getFinalPolyshapeCoordinates(targetPoints);
|
||||
this.release();
|
||||
this.onDrawDone({
|
||||
shapeType: this.drawData.shapeType,
|
||||
points,
|
||||
occluded: this.drawData.initialState.occluded,
|
||||
attributes: { ...this.drawData.initialState.attributes },
|
||||
label: this.drawData.initialState.label,
|
||||
color: this.drawData.initialState.color,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Common settings for rectangle and polyshapes
|
||||
private pasteShape(): void {
|
||||
this.drawInstance.attr({
|
||||
z_order: Number.MAX_SAFE_INTEGER,
|
||||
});
|
||||
|
||||
this.canvas.on('mousemove.draw', (e: MouseEvent): void => {
|
||||
const [x, y] = translateToSVG(
|
||||
this.canvas.node as any as SVGSVGElement,
|
||||
[e.clientX, e.clientY],
|
||||
);
|
||||
|
||||
const bbox = this.drawInstance.bbox();
|
||||
this.drawInstance.move(x - bbox.width / 2, y - bbox.height / 2);
|
||||
});
|
||||
}
|
||||
|
||||
private pasteBox(box: BBox): void {
|
||||
this.drawInstance = (this.canvas as any).rect(box.width, box.height)
|
||||
.move(box.x, box.y)
|
||||
.addClass('cvat_canvas_shape_drawing').style({
|
||||
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
|
||||
});
|
||||
this.pasteShape();
|
||||
|
||||
this.canvas.on('click.draw', (e: MouseEvent): void => {
|
||||
const bbox = (e.target as SVGRectElement).getBBox();
|
||||
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
|
||||
this.release();
|
||||
this.onDrawDone({
|
||||
shapeType: this.drawData.shapeType,
|
||||
points: [xtl, ytl, xbr, ybr],
|
||||
occluded: this.drawData.initialState.occluded,
|
||||
attributes: { ...this.drawData.initialState.attributes },
|
||||
label: this.drawData.initialState.label,
|
||||
color: this.drawData.initialState.color,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private pastePolygon(points: string): void {
|
||||
this.drawInstance = (this.canvas as any).polygon(points)
|
||||
.addClass('cvat_canvas_shape_drawing').style({
|
||||
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
|
||||
});
|
||||
this.pasteShape();
|
||||
this.pastePolyshape();
|
||||
}
|
||||
|
||||
private pastePolyline(points: string): void {
|
||||
this.drawInstance = (this.canvas as any).polyline(points)
|
||||
.addClass('cvat_canvas_shape_drawing').style({
|
||||
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
|
||||
});
|
||||
this.pasteShape();
|
||||
this.pastePolyshape();
|
||||
}
|
||||
|
||||
private pastePoints(points: string): void {
|
||||
this.drawInstance = (this.canvas as any).polyline(points)
|
||||
.addClass('cvat_canvas_shape_drawing').style({
|
||||
'stroke-width': 0,
|
||||
});
|
||||
this.pasteShape();
|
||||
this.pastePolyshape();
|
||||
}
|
||||
|
||||
private startDraw(): void {
|
||||
// TODO: Use enums after typification cvat-core
|
||||
if (this.drawData.initialState) {
|
||||
if (this.drawData.shapeType === 'rectangle') {
|
||||
const [xtl, ytl, xbr, ybr] = translateBetweenSVG(
|
||||
this.background,
|
||||
this.canvas.node as any as SVGSVGElement,
|
||||
this.drawData.initialState.points,
|
||||
);
|
||||
|
||||
this.pasteBox({
|
||||
x: xtl,
|
||||
y: ytl,
|
||||
width: xbr - xtl,
|
||||
height: ybr - ytl,
|
||||
});
|
||||
} else {
|
||||
const points = translateBetweenSVG(
|
||||
this.background,
|
||||
this.canvas.node as any as SVGSVGElement,
|
||||
this.drawData.initialState.points,
|
||||
);
|
||||
|
||||
const stringifiedPoints = pointsToString(points);
|
||||
|
||||
if (this.drawData.shapeType === 'polygon') {
|
||||
this.pastePolygon(stringifiedPoints);
|
||||
} else if (this.drawData.shapeType === 'polyline') {
|
||||
this.pastePolyline(stringifiedPoints);
|
||||
} else if (this.drawData.shapeType === 'points') {
|
||||
this.pastePoints(stringifiedPoints);
|
||||
}
|
||||
}
|
||||
} else if (this.drawData.shapeType === 'rectangle') {
|
||||
this.drawBox();
|
||||
// Draw instance was initialized after drawBox();
|
||||
this.shapeSizeElement = displayShapeSize(this.canvas, this.text);
|
||||
} else if (this.drawData.shapeType === 'polygon') {
|
||||
this.drawPolygon();
|
||||
} else if (this.drawData.shapeType === 'polyline') {
|
||||
this.drawPolyline();
|
||||
} else if (this.drawData.shapeType === 'points') {
|
||||
this.drawPoints();
|
||||
}
|
||||
}
|
||||
|
||||
public constructor(
|
||||
onDrawDone: (data: object) => void,
|
||||
canvas: SVG.Container,
|
||||
text: SVG.Container,
|
||||
background: SVGSVGElement,
|
||||
) {
|
||||
this.onDrawDone = onDrawDone;
|
||||
this.canvas = canvas;
|
||||
this.text = text;
|
||||
this.background = background;
|
||||
this.drawData = null;
|
||||
this.geometry = null;
|
||||
this.crosshair = null;
|
||||
this.drawInstance = null;
|
||||
|
||||
this.canvas.on('mousemove.crosshair', (e: MouseEvent): void => {
|
||||
if (this.crosshair) {
|
||||
const [x, y] = translateToSVG(
|
||||
this.canvas.node as any as SVGSVGElement,
|
||||
[e.clientX, e.clientY],
|
||||
);
|
||||
|
||||
this.crosshair.x.attr({
|
||||
y1: y,
|
||||
y2: y,
|
||||
});
|
||||
|
||||
this.crosshair.y.attr({
|
||||
x1: x,
|
||||
x2: x,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public transform(geometry: Geometry): void {
|
||||
this.geometry = geometry;
|
||||
|
||||
if (this.shapeSizeElement && this.drawInstance && this.drawData.shapeType === 'rectangle') {
|
||||
this.shapeSizeElement.update(this.drawInstance);
|
||||
}
|
||||
|
||||
if (this.crosshair) {
|
||||
this.crosshair.x.attr({
|
||||
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * geometry.scale),
|
||||
});
|
||||
this.crosshair.y.attr({
|
||||
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * geometry.scale),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.drawInstance) {
|
||||
this.drawInstance.draw('transform');
|
||||
this.drawInstance.style({
|
||||
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
|
||||
});
|
||||
|
||||
const paintHandler = this.drawInstance.remember('_paintHandler');
|
||||
|
||||
for (const point of (paintHandler as any).set.members) {
|
||||
point.style(
|
||||
'stroke-width',
|
||||
`${consts.POINTS_STROKE_WIDTH / geometry.scale}`,
|
||||
);
|
||||
point.attr(
|
||||
'r',
|
||||
`${consts.BASE_POINT_SIZE / geometry.scale}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public draw(drawData: DrawData, geometry: Geometry): void {
|
||||
this.geometry = geometry;
|
||||
|
||||
if (drawData.enabled) {
|
||||
this.drawData = drawData;
|
||||
this.initDrawing();
|
||||
this.startDraw();
|
||||
} else {
|
||||
this.closeDrawing();
|
||||
this.drawData = drawData;
|
||||
}
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
this.release();
|
||||
this.onDrawDone(null);
|
||||
// here is a cycle
|
||||
// onDrawDone => controller => model => view => closeDrawing
|
||||
// one call of closeDrawing is unuseful, but it's okey
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,343 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import * as SVG from 'svg.js';
|
||||
import 'svg.select.js';
|
||||
|
||||
import consts from './consts';
|
||||
import {
|
||||
translateFromSVG,
|
||||
translateBetweenSVG,
|
||||
pointsToArray,
|
||||
} from './shared';
|
||||
import {
|
||||
EditData,
|
||||
Geometry,
|
||||
} from './canvasModel';
|
||||
|
||||
export interface EditHandler {
|
||||
edit(editData: EditData): void;
|
||||
transform(geometry: Geometry): void;
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
export class EditHandlerImpl implements EditHandler {
|
||||
private onEditDone: (state: any, points: number[]) => void;
|
||||
private geometry: Geometry;
|
||||
private canvas: SVG.Container;
|
||||
private background: SVGSVGElement;
|
||||
private editData: EditData;
|
||||
private editedShape: SVG.Shape;
|
||||
private editLine: SVG.PolyLine;
|
||||
private clones: SVG.Polygon[];
|
||||
|
||||
private startEdit(): void {
|
||||
// get started coordinates
|
||||
const [clientX, clientY] = translateFromSVG(
|
||||
this.canvas.node as any as SVGSVGElement,
|
||||
this.editedShape.attr('points').split(' ')[this.editData.pointID].split(','),
|
||||
);
|
||||
|
||||
// Add ability to edit shapes by sliding
|
||||
// We need to remember last drawn point
|
||||
// to implementation of slide drawing
|
||||
const lastDrawnPoint: {
|
||||
x: number;
|
||||
y: number;
|
||||
} = {
|
||||
x: null,
|
||||
y: null,
|
||||
};
|
||||
|
||||
const handleSlide = function handleSlide(e: MouseEvent): void {
|
||||
if (e.shiftKey) {
|
||||
if (lastDrawnPoint.x === null || lastDrawnPoint.y === null) {
|
||||
this.editLine.draw('point', e);
|
||||
} else {
|
||||
const deltaTreshold = 15;
|
||||
const delta = Math.sqrt(
|
||||
((e.clientX - lastDrawnPoint.x) ** 2)
|
||||
+ ((e.clientY - lastDrawnPoint.y) ** 2),
|
||||
);
|
||||
if (delta > deltaTreshold) {
|
||||
this.editLine.draw('point', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}.bind(this);
|
||||
this.canvas.on('mousemove.draw', handleSlide);
|
||||
|
||||
this.editLine = (this.canvas as any).polyline().draw({
|
||||
snapToGrid: 0.1,
|
||||
}).addClass('cvat_canvas_shape_drawing').style({
|
||||
'pointer-events': 'none',
|
||||
'fill-opacity': 0,
|
||||
}).on('drawstart drawpoint', (e: CustomEvent): void => {
|
||||
this.transform(this.geometry);
|
||||
lastDrawnPoint.x = e.detail.event.clientX;
|
||||
lastDrawnPoint.y = e.detail.event.clientY;
|
||||
});
|
||||
|
||||
if (this.editData.state.shapeType === 'points') {
|
||||
this.editLine.style('stroke-width', 0);
|
||||
} else {
|
||||
// generate mouse event
|
||||
const dummyEvent = new MouseEvent('mousedown', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clientX,
|
||||
clientY,
|
||||
});
|
||||
(this.editLine as any).draw('point', dummyEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private stopEdit(e: MouseEvent): void {
|
||||
function selectPolygon(shape: SVG.Polygon): void {
|
||||
const points = translateBetweenSVG(
|
||||
this.canvas.node as any as SVGSVGElement,
|
||||
this.background,
|
||||
pointsToArray(shape.attr('points')),
|
||||
);
|
||||
|
||||
const { state } = this.editData;
|
||||
this.edit({
|
||||
enabled: false,
|
||||
});
|
||||
this.onEditDone(state, points);
|
||||
}
|
||||
|
||||
if (!this.editLine) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get stop point and all points
|
||||
const stopPointID = Array.prototype.indexOf
|
||||
.call((e.target as HTMLElement).parentElement.children, e.target);
|
||||
const oldPoints = this.editedShape.attr('points').trim().split(' ');
|
||||
const linePoints = this.editLine.attr('points').trim().split(' ');
|
||||
|
||||
if (this.editLine.attr('points') === '0,0') {
|
||||
this.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute new point array
|
||||
const [start, stop] = [this.editData.pointID, stopPointID]
|
||||
.sort((a, b): number => +a - +b);
|
||||
|
||||
if (this.editData.state.shapeType === 'polygon') {
|
||||
if (start !== this.editData.pointID) {
|
||||
linePoints.reverse();
|
||||
}
|
||||
|
||||
const firstPart = oldPoints.slice(0, start)
|
||||
.concat(linePoints)
|
||||
.concat(oldPoints.slice(stop + 1));
|
||||
|
||||
linePoints.reverse();
|
||||
const secondPart = oldPoints.slice(start + 1, stop)
|
||||
.concat(linePoints);
|
||||
|
||||
if (firstPart.length < 3 || secondPart.length < 3) {
|
||||
this.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const points of [firstPart, secondPart]) {
|
||||
this.clones.push(this.canvas.polygon(points.join(' '))
|
||||
.attr('fill', this.editedShape.attr('fill'))
|
||||
.style('fill-opacity', '0.5')
|
||||
.addClass('cvat_canvas_shape'));
|
||||
}
|
||||
|
||||
for (const clone of this.clones) {
|
||||
clone.on('click', selectPolygon.bind(this, clone));
|
||||
clone.on('mouseenter', (): void => {
|
||||
clone.addClass('cvat_canvas_shape_splitting');
|
||||
}).on('mouseleave', (): void => {
|
||||
clone.removeClass('cvat_canvas_shape_splitting');
|
||||
});
|
||||
}
|
||||
|
||||
(this.editLine as any).draw('stop');
|
||||
this.editLine.remove();
|
||||
this.editLine = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let points = null;
|
||||
if (this.editData.state.shapeType === 'polyline') {
|
||||
if (start !== this.editData.pointID) {
|
||||
linePoints.reverse();
|
||||
}
|
||||
points = oldPoints.slice(0, start)
|
||||
.concat(linePoints)
|
||||
.concat(oldPoints.slice(stop + 1));
|
||||
} else {
|
||||
points = oldPoints.concat(linePoints.slice(0, -1));
|
||||
}
|
||||
|
||||
points = translateBetweenSVG(
|
||||
this.canvas.node as any as SVGSVGElement,
|
||||
this.background,
|
||||
pointsToArray(points.join(' ')),
|
||||
);
|
||||
|
||||
const { state } = this.editData;
|
||||
this.edit({
|
||||
enabled: false,
|
||||
});
|
||||
this.onEditDone(state, points);
|
||||
}
|
||||
|
||||
private setupPoints(enabled: boolean): void {
|
||||
const self = this;
|
||||
const stopEdit = self.stopEdit.bind(self);
|
||||
|
||||
if (enabled) {
|
||||
(this.editedShape as any).selectize(true, {
|
||||
deepSelect: true,
|
||||
pointSize: 2 * consts.BASE_POINT_SIZE / self.geometry.scale,
|
||||
rotationPoint: false,
|
||||
pointType(cx: number, cy: number): SVG.Circle {
|
||||
const circle: SVG.Circle = this.nested
|
||||
.circle(this.options.pointSize)
|
||||
.stroke('black')
|
||||
.fill(self.editedShape.attr('fill') || 'inherit')
|
||||
.center(cx, cy)
|
||||
.attr({
|
||||
'stroke-width': consts.POINTS_STROKE_WIDTH / self.geometry.scale,
|
||||
});
|
||||
|
||||
circle.node.addEventListener('mouseenter', (): void => {
|
||||
circle.attr({
|
||||
'stroke-width': consts.POINTS_SELECTED_STROKE_WIDTH / self.geometry.scale,
|
||||
});
|
||||
|
||||
circle.node.addEventListener('click', stopEdit);
|
||||
circle.addClass('cvat_canvas_selected_point');
|
||||
});
|
||||
|
||||
circle.node.addEventListener('mouseleave', (): void => {
|
||||
circle.attr({
|
||||
'stroke-width': consts.POINTS_STROKE_WIDTH / self.geometry.scale,
|
||||
});
|
||||
|
||||
circle.node.removeEventListener('click', stopEdit);
|
||||
circle.removeClass('cvat_canvas_selected_point');
|
||||
});
|
||||
|
||||
return circle;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
(this.editedShape as any).selectize(false, {
|
||||
deepSelect: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private release(): void {
|
||||
this.canvas.off('mousemove.draw');
|
||||
|
||||
if (this.editedShape) {
|
||||
this.setupPoints(false);
|
||||
this.editedShape.remove();
|
||||
this.editedShape = null;
|
||||
}
|
||||
|
||||
if (this.editLine) {
|
||||
(this.editLine as any).draw('stop');
|
||||
this.editLine.remove();
|
||||
this.editLine = null;
|
||||
}
|
||||
|
||||
if (this.clones.length) {
|
||||
for (const clone of this.clones) {
|
||||
clone.remove();
|
||||
}
|
||||
this.clones = [];
|
||||
}
|
||||
}
|
||||
|
||||
private initEditing(): void {
|
||||
this.editedShape = this.canvas
|
||||
.select(`#cvat_canvas_shape_${this.editData.state.clientID}`)
|
||||
.first().clone();
|
||||
this.setupPoints(true);
|
||||
this.startEdit();
|
||||
// draw points for this with selected and start editing till another point is clicked
|
||||
// click one of two parts to remove (in case of polygon only)
|
||||
|
||||
// else we can start draw polyline
|
||||
// after we have got shape and points, we are waiting for second point pressed on this shape
|
||||
}
|
||||
|
||||
private closeEditing(): void {
|
||||
this.release();
|
||||
}
|
||||
|
||||
public constructor(
|
||||
onEditDone: (state: any, points: number[]) => void,
|
||||
canvas: SVG.Container,
|
||||
background: SVGSVGElement,
|
||||
) {
|
||||
this.onEditDone = onEditDone;
|
||||
this.canvas = canvas;
|
||||
this.background = background;
|
||||
this.editData = null;
|
||||
this.editedShape = null;
|
||||
this.editLine = null;
|
||||
this.geometry = null;
|
||||
this.clones = [];
|
||||
}
|
||||
|
||||
public edit(editData: any): void {
|
||||
if (editData.enabled) {
|
||||
if (editData.state.shapeType !== 'rectangle') {
|
||||
this.editData = editData;
|
||||
this.initEditing();
|
||||
} else {
|
||||
this.cancel();
|
||||
}
|
||||
} else {
|
||||
this.closeEditing();
|
||||
this.editData = editData;
|
||||
}
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
this.release();
|
||||
this.onEditDone(null, null);
|
||||
}
|
||||
|
||||
public transform(geometry: Geometry): void {
|
||||
this.geometry = geometry;
|
||||
|
||||
if (this.editLine) {
|
||||
(this.editLine as any).draw('transform');
|
||||
if (this.editData.state.shapeType !== 'points') {
|
||||
this.editLine.style({
|
||||
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
|
||||
});
|
||||
}
|
||||
|
||||
const paintHandler = this.editLine.remember('_paintHandler');
|
||||
|
||||
for (const point of (paintHandler as any).set.members) {
|
||||
point.style(
|
||||
'stroke-width',
|
||||
`${consts.POINTS_STROKE_WIDTH / geometry.scale}`,
|
||||
);
|
||||
point.attr(
|
||||
'r',
|
||||
`${consts.BASE_POINT_SIZE / geometry.scale}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,208 @@
|
||||
import * as SVG from 'svg.js';
|
||||
import { GroupData } from './canvasModel';
|
||||
|
||||
import {
|
||||
translateToSVG,
|
||||
} from './shared';
|
||||
|
||||
|
||||
export interface GroupHandler {
|
||||
group(groupData: GroupData): void;
|
||||
select(state: any): void;
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
export class GroupHandlerImpl implements GroupHandler {
|
||||
// callback is used to notify about grouping end
|
||||
private onGroupDone: (objects: any[]) => void;
|
||||
private getStates: () => any[];
|
||||
private onFindObject: (event: MouseEvent) => void;
|
||||
private onSelectStart: (event: MouseEvent) => void;
|
||||
private onSelectUpdate: (event: MouseEvent) => void;
|
||||
private onSelectStop: (event: MouseEvent) => void;
|
||||
private selectionRect: SVG.Rect;
|
||||
private startSelectionPoint: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
private canvas: SVG.Container;
|
||||
private initialized: boolean;
|
||||
private states: any[];
|
||||
private highlightedShapes: Record<number, SVG.Shape>;
|
||||
|
||||
private getSelectionBox(event: MouseEvent): {
|
||||
xtl: number;
|
||||
ytl: number;
|
||||
xbr: number;
|
||||
ybr: number;
|
||||
} {
|
||||
const point = translateToSVG(
|
||||
(this.canvas.node as any as SVGSVGElement),
|
||||
[event.clientX, event.clientY],
|
||||
);
|
||||
const stopSelectionPoint = {
|
||||
x: point[0],
|
||||
y: point[1],
|
||||
};
|
||||
|
||||
return {
|
||||
xtl: Math.min(this.startSelectionPoint.x, stopSelectionPoint.x),
|
||||
ytl: Math.min(this.startSelectionPoint.y, stopSelectionPoint.y),
|
||||
xbr: Math.max(this.startSelectionPoint.x, stopSelectionPoint.x),
|
||||
ybr: Math.max(this.startSelectionPoint.y, stopSelectionPoint.y),
|
||||
};
|
||||
}
|
||||
|
||||
private release(): void {
|
||||
this.canvas.node.removeEventListener('click', this.onFindObject);
|
||||
this.canvas.node.removeEventListener('mousedown', this.onSelectStart);
|
||||
this.canvas.node.removeEventListener('mousemove', this.onSelectUpdate);
|
||||
this.canvas.node.removeEventListener('mouseup', this.onSelectStop);
|
||||
this.canvas.node.removeEventListener('mouseleave', this.onSelectStop);
|
||||
|
||||
for (const state of this.states) {
|
||||
const shape = this.highlightedShapes[state.clientID];
|
||||
shape.removeClass('cvat_canvas_shape_grouping');
|
||||
}
|
||||
this.states = [];
|
||||
this.highlightedShapes = {};
|
||||
this.initialized = false;
|
||||
this.selectionRect = null;
|
||||
this.startSelectionPoint = {
|
||||
x: null,
|
||||
y: null,
|
||||
};
|
||||
}
|
||||
|
||||
private initGrouping(): void {
|
||||
this.canvas.node.addEventListener('click', this.onFindObject);
|
||||
this.canvas.node.addEventListener('mousedown', this.onSelectStart);
|
||||
this.canvas.node.addEventListener('mousemove', this.onSelectUpdate);
|
||||
this.canvas.node.addEventListener('mouseup', this.onSelectStop);
|
||||
this.canvas.node.addEventListener('mouseleave', this.onSelectStop);
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
private closeGrouping(): void {
|
||||
if (this.initialized) {
|
||||
const { states } = this;
|
||||
this.release();
|
||||
|
||||
if (states.length) {
|
||||
this.onGroupDone(states);
|
||||
} else {
|
||||
this.onGroupDone(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public constructor(
|
||||
onGroupDone: (objects: any[]) => void,
|
||||
getStates: () => any[],
|
||||
onFindObject: (event: MouseEvent) => void,
|
||||
canvas: SVG.Container,
|
||||
) {
|
||||
this.onGroupDone = onGroupDone;
|
||||
this.getStates = getStates;
|
||||
this.onFindObject = onFindObject;
|
||||
this.canvas = canvas;
|
||||
this.states = [];
|
||||
this.highlightedShapes = {};
|
||||
this.selectionRect = null;
|
||||
this.startSelectionPoint = {
|
||||
x: null,
|
||||
y: null,
|
||||
};
|
||||
|
||||
this.onSelectStart = function (event: MouseEvent): void {
|
||||
if (!this.selectionRect) {
|
||||
const point = translateToSVG(this.canvas.node, [event.clientX, event.clientY]);
|
||||
this.startSelectionPoint = {
|
||||
x: point[0],
|
||||
y: point[1],
|
||||
};
|
||||
|
||||
this.selectionRect = this.canvas.rect().addClass('cvat_canvas_shape_grouping');
|
||||
this.selectionRect.attr({ ...this.startSelectionPoint });
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
this.onSelectUpdate = function (event: MouseEvent): void {
|
||||
// called on mousemove
|
||||
if (this.selectionRect) {
|
||||
const box = this.getSelectionBox(event);
|
||||
|
||||
this.selectionRect.attr({
|
||||
x: box.xtl,
|
||||
y: box.ytl,
|
||||
width: box.xbr - box.xtl,
|
||||
height: box.ybr - box.ytl,
|
||||
});
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
this.onSelectStop = function (event: MouseEvent): void {
|
||||
// called on mouseup, mouseleave
|
||||
if (this.selectionRect) {
|
||||
this.selectionRect.remove();
|
||||
this.selectionRect = null;
|
||||
|
||||
const box = this.getSelectionBox(event);
|
||||
const shapes = (this.canvas.select('.cvat_canvas_shape') as any).members;
|
||||
for (const shape of shapes) {
|
||||
// TODO: Doesn't work properly for groups
|
||||
const bbox = shape.bbox();
|
||||
const clientID = shape.attr('clientID');
|
||||
if (bbox.x > box.xtl && bbox.y > box.ytl
|
||||
&& bbox.x + bbox.width < box.xbr
|
||||
&& bbox.y + bbox.height < box.ybr
|
||||
&& !(clientID in this.highlightedShapes)) {
|
||||
const objectState = this.getStates()
|
||||
.filter((state: any): boolean => state.clientID === clientID)[0];
|
||||
|
||||
if (objectState) {
|
||||
this.states.push(objectState);
|
||||
this.highlightedShapes[clientID] = shape;
|
||||
(shape as any).addClass('cvat_canvas_shape_grouping');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.bind(this);
|
||||
}
|
||||
|
||||
/* eslint-disable-next-line */
|
||||
public group(groupData: GroupData): void {
|
||||
if (groupData.enabled) {
|
||||
this.initGrouping();
|
||||
} else {
|
||||
this.closeGrouping();
|
||||
}
|
||||
}
|
||||
|
||||
public select(objectState: any): void {
|
||||
const stateIndexes = this.states.map((state): number => state.clientID);
|
||||
const includes = stateIndexes.indexOf(objectState.clientID);
|
||||
if (includes !== -1) {
|
||||
const shape = this.highlightedShapes[objectState.clientID];
|
||||
this.states.splice(includes, 1);
|
||||
if (shape) {
|
||||
delete this.highlightedShapes[objectState.clientID];
|
||||
shape.removeClass('cvat_canvas_shape_grouping');
|
||||
}
|
||||
} else {
|
||||
const shape = this.canvas.select(`#cvat_canvas_shape_${objectState.clientID}`).first();
|
||||
if (shape) {
|
||||
this.states.push(objectState);
|
||||
this.highlightedShapes[objectState.clientID] = shape;
|
||||
shape.addClass('cvat_canvas_shape_grouping');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
this.release();
|
||||
this.onGroupDone(null);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export interface Master {
|
||||
subscribe(listener: Listener): void;
|
||||
unsubscribe(listener: Listener): void;
|
||||
unsubscribeAll(): void;
|
||||
notify(reason: string): void;
|
||||
}
|
||||
|
||||
export interface Listener {
|
||||
notify(master: Master, reason: string): void;
|
||||
}
|
||||
|
||||
export class MasterImpl implements Master {
|
||||
private listeners: Listener[];
|
||||
|
||||
public constructor() {
|
||||
this.listeners = [];
|
||||
}
|
||||
|
||||
public subscribe(listener: Listener): void {
|
||||
this.listeners.push(listener);
|
||||
}
|
||||
|
||||
public unsubscribe(listener: Listener): void {
|
||||
for (let i = 0; i < this.listeners.length; i++) {
|
||||
if (this.listeners[i] === listener) {
|
||||
this.listeners.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public unsubscribeAll(): void {
|
||||
this.listeners = [];
|
||||
}
|
||||
|
||||
public notify(reason: string): void {
|
||||
for (const listener of this.listeners) {
|
||||
listener.notify(this, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,133 @@
|
||||
import * as SVG from 'svg.js';
|
||||
import { MergeData } from './canvasModel';
|
||||
|
||||
export interface MergeHandler {
|
||||
merge(mergeData: MergeData): void;
|
||||
select(state: any): void;
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
|
||||
export class MergeHandlerImpl implements MergeHandler {
|
||||
// callback is used to notify about merging end
|
||||
private onMergeDone: (objects: any[]) => void;
|
||||
private onFindObject: (event: MouseEvent) => void;
|
||||
private canvas: SVG.Container;
|
||||
private initialized: boolean;
|
||||
private states: any[]; // are being merged
|
||||
private highlightedShapes: Record<number, SVG.Shape>;
|
||||
private constraints: {
|
||||
labelID: number;
|
||||
shapeType: string;
|
||||
};
|
||||
|
||||
private addConstraints(): void {
|
||||
const shape = this.states[0];
|
||||
this.constraints = {
|
||||
labelID: shape.label.id,
|
||||
shapeType: shape.shapeType,
|
||||
};
|
||||
}
|
||||
|
||||
private removeConstraints(): void {
|
||||
this.constraints = null;
|
||||
}
|
||||
|
||||
private checkConstraints(state: any): boolean {
|
||||
return !this.constraints || (state.label.id === this.constraints.labelID
|
||||
&& state.shapeType === this.constraints.shapeType);
|
||||
}
|
||||
|
||||
private release(): void {
|
||||
this.removeConstraints();
|
||||
this.canvas.node.removeEventListener('click', this.onFindObject);
|
||||
for (const state of this.states) {
|
||||
const shape = this.highlightedShapes[state.clientID];
|
||||
shape.removeClass('cvat_canvas_shape_merging');
|
||||
}
|
||||
this.states = [];
|
||||
this.highlightedShapes = {};
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
private initMerging(): void {
|
||||
this.canvas.node.addEventListener('click', this.onFindObject);
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
private closeMerging(): void {
|
||||
if (this.initialized) {
|
||||
const { states } = this;
|
||||
this.release();
|
||||
|
||||
if (states.length > 1) {
|
||||
this.onMergeDone(states);
|
||||
} else {
|
||||
this.onMergeDone(null);
|
||||
// here is a cycle
|
||||
// onMergeDone => controller => model => view => closeMerging
|
||||
// one call of closeMerging is unuseful, but it's okey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public constructor(
|
||||
onMergeDone: (objects: any[]) => void,
|
||||
onFindObject: (event: MouseEvent) => void,
|
||||
canvas: SVG.Container,
|
||||
) {
|
||||
this.onMergeDone = onMergeDone;
|
||||
this.onFindObject = onFindObject;
|
||||
this.canvas = canvas;
|
||||
this.states = [];
|
||||
this.highlightedShapes = {};
|
||||
this.constraints = null;
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
public merge(mergeData: MergeData): void {
|
||||
if (mergeData.enabled) {
|
||||
this.initMerging();
|
||||
} else {
|
||||
this.closeMerging();
|
||||
}
|
||||
}
|
||||
|
||||
public select(objectState: any): void {
|
||||
const stateIndexes = this.states.map((state): number => state.clientID);
|
||||
const stateFrames = this.states.map((state): number => state.frame);
|
||||
const includes = stateIndexes.indexOf(objectState.clientID);
|
||||
if (includes !== -1) {
|
||||
const shape = this.highlightedShapes[objectState.clientID];
|
||||
this.states.splice(includes, 1);
|
||||
if (shape) {
|
||||
delete this.highlightedShapes[objectState.clientID];
|
||||
shape.removeClass('cvat_canvas_shape_merging');
|
||||
}
|
||||
|
||||
if (!this.states.length) {
|
||||
this.removeConstraints();
|
||||
}
|
||||
} else {
|
||||
const shape = this.canvas.select(`#cvat_canvas_shape_${objectState.clientID}`).first();
|
||||
if (shape && this.checkConstraints(objectState)
|
||||
&& !stateFrames.includes(objectState.frame)) {
|
||||
this.states.push(objectState);
|
||||
this.highlightedShapes[objectState.clientID] = shape;
|
||||
shape.addClass('cvat_canvas_shape_merging');
|
||||
|
||||
if (this.states.length === 1) {
|
||||
this.addConstraints();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
this.release();
|
||||
this.onMergeDone(null);
|
||||
// here is a cycle
|
||||
// onMergeDone => controller => model => view => closeMerging
|
||||
// one call of closeMerging is unuseful, but it's okey
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import * as SVG from 'svg.js';
|
||||
import consts from './consts';
|
||||
|
||||
export interface ShapeSizeElement {
|
||||
sizeElement: any;
|
||||
update(shape: SVG.Shape): void;
|
||||
rm(): void;
|
||||
}
|
||||
|
||||
export interface Box {
|
||||
xtl: number;
|
||||
ytl: number;
|
||||
xbr: number;
|
||||
ybr: number;
|
||||
}
|
||||
|
||||
export interface BBox {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
// Translate point array from the client coordinate system
|
||||
// to a coordinate system of a canvas
|
||||
export function translateFromSVG(svg: SVGSVGElement, points: number[]): number[] {
|
||||
const output = [];
|
||||
const transformationMatrix = svg.getScreenCTM();
|
||||
let pt = svg.createSVGPoint();
|
||||
for (let i = 0; i < points.length - 1; i += 2) {
|
||||
pt.x = points[i];
|
||||
pt.y = points[i + 1];
|
||||
pt = pt.matrixTransform(transformationMatrix);
|
||||
output.push(pt.x, pt.y);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// Translate point array from a coordinate system of a canvas
|
||||
// to the client coordinate system
|
||||
export function translateToSVG(svg: SVGSVGElement, points: number[]): number[] {
|
||||
const output = [];
|
||||
const transformationMatrix = svg.getScreenCTM().inverse();
|
||||
let pt = svg.createSVGPoint();
|
||||
for (let i = 0; i < points.length; i += 2) {
|
||||
pt.x = points[i];
|
||||
pt.y = points[i + 1];
|
||||
pt = pt.matrixTransform(transformationMatrix);
|
||||
output.push(pt.x, pt.y);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// Translate point array from the first canvas coordinate system
|
||||
// to another
|
||||
export function translateBetweenSVG(
|
||||
from: SVGSVGElement,
|
||||
to: SVGSVGElement,
|
||||
points: number[],
|
||||
): number[] {
|
||||
return translateToSVG(to, translateFromSVG(from, points));
|
||||
}
|
||||
|
||||
export function pointsToString(points: number[]): string {
|
||||
return points.reduce((acc, val, idx): string => {
|
||||
if (idx % 2) {
|
||||
return `${acc},${val}`;
|
||||
}
|
||||
|
||||
return `${acc} ${val}`;
|
||||
}, '');
|
||||
}
|
||||
|
||||
export function pointsToArray(points: string): number[] {
|
||||
return points.trim().split(/[,\s]+/g)
|
||||
.map((coord: string): number => +coord);
|
||||
}
|
||||
|
||||
export function displayShapeSize(
|
||||
shapesContainer: SVG.Container,
|
||||
textContainer: SVG.Container,
|
||||
): ShapeSizeElement {
|
||||
const shapeSize: ShapeSizeElement = {
|
||||
sizeElement: textContainer.text('').font({
|
||||
weight: 'bolder',
|
||||
}).fill('white').addClass('cvat_canvas_text'),
|
||||
update(shape: SVG.Shape): void{
|
||||
const bbox = shape.bbox();
|
||||
const text = `${bbox.width.toFixed(1)}x${bbox.height.toFixed(1)}`;
|
||||
const [x, y]: number[] = translateToSVG(
|
||||
textContainer.node as any as SVGSVGElement,
|
||||
translateFromSVG((shapesContainer.node as any as SVGSVGElement), [bbox.x, bbox.y]),
|
||||
);
|
||||
this.sizeElement.clear().plain(text)
|
||||
.move(x + consts.TEXT_MARGIN, y + consts.TEXT_MARGIN);
|
||||
},
|
||||
rm(): void {
|
||||
if (this.sizeElement) {
|
||||
this.sizeElement.remove();
|
||||
this.sizeElement = null;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return shapeSize;
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
import * as SVG from 'svg.js';
|
||||
import { SplitData } from './canvasModel';
|
||||
|
||||
export interface SplitHandler {
|
||||
split(splitData: SplitData): void;
|
||||
select(state: any): void;
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
export class SplitHandlerImpl implements SplitHandler {
|
||||
// callback is used to notify about splitting end
|
||||
private onSplitDone: (object: any) => void;
|
||||
private onFindObject: (event: MouseEvent) => void;
|
||||
private canvas: SVG.Container;
|
||||
private highlightedShape: SVG.Shape;
|
||||
private initialized: boolean;
|
||||
private splitDone: boolean;
|
||||
|
||||
private resetShape(): void {
|
||||
if (this.highlightedShape) {
|
||||
this.highlightedShape.removeClass('cvat_canvas_shape_splitting');
|
||||
this.highlightedShape.off('click.split');
|
||||
this.highlightedShape = null;
|
||||
}
|
||||
}
|
||||
|
||||
private release(): void {
|
||||
if (this.initialized) {
|
||||
this.resetShape();
|
||||
this.canvas.node.removeEventListener('mousemove', this.onFindObject);
|
||||
this.initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
private initSplitting(): void {
|
||||
this.canvas.node.addEventListener('mousemove', this.onFindObject);
|
||||
this.initialized = true;
|
||||
this.splitDone = false;
|
||||
}
|
||||
|
||||
private closeSplitting(): void {
|
||||
// Split done is true if an object was splitted
|
||||
// Split also can be called with { enabled: false } without splitting an object
|
||||
if (!this.splitDone) {
|
||||
this.onSplitDone(null);
|
||||
}
|
||||
this.release();
|
||||
}
|
||||
|
||||
public constructor(
|
||||
onSplitDone: (object: any) => void,
|
||||
onFindObject: (event: MouseEvent) => void,
|
||||
canvas: SVG.Container,
|
||||
) {
|
||||
this.onSplitDone = onSplitDone;
|
||||
this.onFindObject = onFindObject;
|
||||
this.canvas = canvas;
|
||||
this.highlightedShape = null;
|
||||
this.initialized = false;
|
||||
this.splitDone = false;
|
||||
}
|
||||
|
||||
public split(splitData: SplitData): void {
|
||||
if (splitData.enabled) {
|
||||
this.initSplitting();
|
||||
} else {
|
||||
this.closeSplitting();
|
||||
}
|
||||
}
|
||||
|
||||
public select(state: any): void {
|
||||
if (state.objectType === 'track') {
|
||||
const shape = this.canvas.select(`#cvat_canvas_shape_${state.clientID}`).first();
|
||||
if (shape && shape !== this.highlightedShape) {
|
||||
this.resetShape();
|
||||
this.highlightedShape = shape;
|
||||
this.highlightedShape.addClass('cvat_canvas_shape_splitting');
|
||||
this.canvas.node.append(this.highlightedShape.node);
|
||||
this.highlightedShape.on('click.split', (): void => {
|
||||
this.splitDone = true;
|
||||
this.onSplitDone(state);
|
||||
}, {
|
||||
once: true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.resetShape();
|
||||
}
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
this.release();
|
||||
this.onSplitDone(null);
|
||||
// here is a cycle
|
||||
// onSplitDone => controller => model => view => closeSplitting
|
||||
// one call of closeMerging is unuseful, but it's okey
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,172 @@
|
||||
import * as SVG from 'svg.js';
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
import 'svg.draggable.js';
|
||||
import 'svg.resize.js';
|
||||
import 'svg.select.js';
|
||||
import 'svg.draw.js';
|
||||
|
||||
// Update constructor
|
||||
const originalDraw = SVG.Element.prototype.draw;
|
||||
SVG.Element.prototype.draw = function constructor(...args: any): any {
|
||||
let handler = this.remember('_paintHandler');
|
||||
if (!handler) {
|
||||
originalDraw.call(this, ...args);
|
||||
handler = this.remember('_paintHandler');
|
||||
handler.set = new SVG.Set();
|
||||
} else {
|
||||
originalDraw.call(this, ...args);
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
for (const key of Object.keys(originalDraw)) {
|
||||
SVG.Element.prototype.draw[key] = originalDraw[key];
|
||||
}
|
||||
|
||||
// Create undo for polygones and polylines
|
||||
function undo(): void {
|
||||
if (this.set.length()) {
|
||||
this.set.members.splice(-1, 1)[0].remove();
|
||||
this.el.array().value.splice(-2, 1);
|
||||
this.el.plot(this.el.array());
|
||||
this.el.fire('undopoint');
|
||||
}
|
||||
}
|
||||
|
||||
SVG.Element.prototype.draw.extend('polyline', Object.assign({},
|
||||
SVG.Element.prototype.draw.plugins.polyline,
|
||||
{
|
||||
undo: undo,
|
||||
},
|
||||
));
|
||||
|
||||
SVG.Element.prototype.draw.extend('polygon', Object.assign({},
|
||||
SVG.Element.prototype.draw.plugins.polygon,
|
||||
{
|
||||
undo: undo,
|
||||
},
|
||||
));
|
||||
|
||||
|
||||
// Create transform for rect, polyline and polygon
|
||||
function transform(): void {
|
||||
this.m = this.el.node.getScreenCTM().inverse();
|
||||
this.offset = { x: window.pageXOffset, y: window.pageYOffset };
|
||||
}
|
||||
|
||||
SVG.Element.prototype.draw.extend('rect', Object.assign({},
|
||||
SVG.Element.prototype.draw.plugins.rect,
|
||||
{
|
||||
transform: transform,
|
||||
},
|
||||
));
|
||||
|
||||
SVG.Element.prototype.draw.extend('polyline', Object.assign({},
|
||||
SVG.Element.prototype.draw.plugins.polyline,
|
||||
{
|
||||
transform: transform,
|
||||
},
|
||||
));
|
||||
|
||||
SVG.Element.prototype.draw.extend('polygon', Object.assign({},
|
||||
SVG.Element.prototype.draw.plugins.polygon,
|
||||
{
|
||||
transform: transform,
|
||||
},
|
||||
));
|
||||
|
||||
// Fix method drawCircles
|
||||
function drawCircles(): void {
|
||||
const array = this.el.array().valueOf();
|
||||
|
||||
this.set.each(function (): void {
|
||||
this.remove();
|
||||
});
|
||||
|
||||
this.set.clear();
|
||||
|
||||
for (let i = 0; i < array.length - 1; ++i) {
|
||||
[this.p.x] = array[i];
|
||||
[, this.p.y] = array[i];
|
||||
|
||||
const 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('line', Object.assign({},
|
||||
SVG.Element.prototype.draw.plugins.line,
|
||||
{
|
||||
drawCircles: drawCircles,
|
||||
}
|
||||
));
|
||||
|
||||
SVG.Element.prototype.draw.extend('polyline', Object.assign({},
|
||||
SVG.Element.prototype.draw.plugins.polyline,
|
||||
{
|
||||
drawCircles: drawCircles,
|
||||
}
|
||||
));
|
||||
|
||||
SVG.Element.prototype.draw.extend('polygon', Object.assign({},
|
||||
SVG.Element.prototype.draw.plugins.polygon,
|
||||
{
|
||||
drawCircles: drawCircles,
|
||||
}
|
||||
));
|
||||
|
||||
// Fix method drag
|
||||
const originalDraggable = SVG.Element.prototype.draggable;
|
||||
SVG.Element.prototype.draggable = function constructor(...args: any): any {
|
||||
let handler = this.remember('_draggable');
|
||||
if (!handler) {
|
||||
originalDraggable.call(this, ...args);
|
||||
handler = this.remember('_draggable');
|
||||
handler.drag = function(e: any) {
|
||||
this.m = this.el.node.getScreenCTM().inverse();
|
||||
return handler.constructor.prototype.drag.call(this, e);
|
||||
}
|
||||
} else {
|
||||
originalDraggable.call(this, ...args);
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
for (const key of Object.keys(originalDraggable)) {
|
||||
SVG.Element.prototype.draggable[key] = originalDraggable[key];
|
||||
}
|
||||
|
||||
// Fix method resize
|
||||
const originalResize = SVG.Element.prototype.resize;
|
||||
SVG.Element.prototype.resize = function constructor(...args: any): any {
|
||||
let handler = this.remember('_resizeHandler');
|
||||
if (!handler) {
|
||||
originalResize.call(this, ...args);
|
||||
handler = this.remember('_resizeHandler');
|
||||
handler.update = function(e: any) {
|
||||
this.m = this.el.node.getScreenCTM().inverse();
|
||||
return handler.constructor.prototype.update.call(this, e);
|
||||
}
|
||||
} else {
|
||||
originalResize.call(this, ...args);
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
for (const key of Object.keys(originalResize)) {
|
||||
SVG.Element.prototype.resize[key] = originalResize[key];
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"emitDeclarationOnly": true,
|
||||
"module": "es6",
|
||||
"target": "es6",
|
||||
"noImplicitAny": true,
|
||||
"preserveConstEnums": true,
|
||||
"declaration": true,
|
||||
"moduleResolution": "node",
|
||||
"declarationDir": "dist/declaration",
|
||||
"paths": {
|
||||
"cvat-canvas.node": ["dist/cvat-canvas.node"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/typescript/*.ts"
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
/* eslint-disable */
|
||||
const path = require('path');
|
||||
const DtsBundleWebpack = require('dts-bundle-webpack')
|
||||
|
||||
const nodeConfig = {
|
||||
target: 'node',
|
||||
mode: 'production',
|
||||
devtool: 'source-map',
|
||||
entry: './src/typescript/canvas.ts',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'cvat-canvas.node.js',
|
||||
library: 'canvas',
|
||||
libraryTarget: 'commonjs',
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js', '.json'],
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
['@babel/preset-env'],
|
||||
['@babel/typescript'],
|
||||
],
|
||||
sourceType: 'unambiguous',
|
||||
},
|
||||
},
|
||||
}, {
|
||||
test: /\.css$/,
|
||||
use: ['style-loader', 'css-loader']
|
||||
}],
|
||||
},
|
||||
plugins: [
|
||||
new DtsBundleWebpack({
|
||||
name: 'cvat-canvas.node',
|
||||
main: 'dist/declaration/canvas.d.ts',
|
||||
out: '../cvat-canvas.node.d.ts',
|
||||
}),
|
||||
]
|
||||
};
|
||||
|
||||
const webConfig = {
|
||||
target: 'web',
|
||||
mode: 'production',
|
||||
devtool: 'source-map',
|
||||
entry: './src/typescript/canvas.ts',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'cvat-canvas.js',
|
||||
library: 'canvas',
|
||||
libraryTarget: 'window',
|
||||
},
|
||||
devServer: {
|
||||
contentBase: path.join(__dirname, 'dist'),
|
||||
compress: false,
|
||||
inline: true,
|
||||
port: 3000,
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js', '.json'],
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
['@babel/preset-env'],
|
||||
['@babel/typescript'],
|
||||
],
|
||||
sourceType: 'unambiguous',
|
||||
},
|
||||
},
|
||||
}, {
|
||||
test: /\.css$/,
|
||||
use: ['style-loader', 'css-loader']
|
||||
}],
|
||||
},
|
||||
plugins: [
|
||||
new DtsBundleWebpack({
|
||||
name: 'cvat-canvas',
|
||||
main: 'dist/declaration/canvas.d.ts',
|
||||
out: '../cvat-canvas.d.ts',
|
||||
}),
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = [webConfig, nodeConfig]
|
||||
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
"env": {
|
||||
"node": false,
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"jquery": true,
|
||||
"qunit": true,
|
||||
},
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint",
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 2018,
|
||||
},
|
||||
"plugins": [
|
||||
"security",
|
||||
"no-unsanitized",
|
||||
"no-unsafe-innerhtml",
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:security/recommended",
|
||||
"plugin:no-unsanitized/DOM",
|
||||
"airbnb-base",
|
||||
],
|
||||
"rules": {
|
||||
"no-await-in-loop": [0],
|
||||
"global-require": [0],
|
||||
"no-new": [0],
|
||||
"class-methods-use-this": [0],
|
||||
"no-restricted-properties": [0, {
|
||||
"object": "Math",
|
||||
"property": "pow",
|
||||
}],
|
||||
"no-plusplus": [0],
|
||||
"no-param-reassign": [0],
|
||||
"no-underscore-dangle": ["error", { "allowAfterThis": true }],
|
||||
"no-restricted-syntax": [0, {"selector": "ForOfStatement"}],
|
||||
"no-continue": [0],
|
||||
"no-unsafe-innerhtml/no-unsafe-innerhtml": 1,
|
||||
// This rule actual for user input data on the node.js environment mainly.
|
||||
"security/detect-object-injection": 0,
|
||||
"indent": ["warn", 4],
|
||||
"no-useless-constructor": 0,
|
||||
"func-names": [0],
|
||||
"valid-typeof": [0],
|
||||
"no-console": [0], // this rule deprecates console.log, console.warn etc. because "it is not good in production code"
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,6 @@
|
||||
docs
|
||||
node_modules
|
||||
reports
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
dist
|
||||
@ -0,0 +1,39 @@
|
||||
# Module CVAT-CORE
|
||||
|
||||
## Description
|
||||
This CVAT module is a clien-side JavaScipt library to management of objects, frames, logs, etc.
|
||||
It contains the core logic of the Computer Vision Annotation Tool.
|
||||
|
||||
### Commands
|
||||
|
||||
- Dependencies installation
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
- Building the module from sources in the ```dist``` directory:
|
||||
```bash
|
||||
npm run build
|
||||
npm run build -- --mode=development # without a minification
|
||||
```
|
||||
|
||||
- Building the documentation in the ```docs``` directory:
|
||||
```bash
|
||||
npm run-script docs
|
||||
```
|
||||
|
||||
- Running of tests:
|
||||
```bash
|
||||
npm run-script test
|
||||
```
|
||||
|
||||
- Updating of a module version:
|
||||
```bash
|
||||
npm version patch # updated after minor fixes
|
||||
npm version minor # updated after major changes which don't affect API compatibility with previous versions
|
||||
npm version major # updated after major changes which affect API compatibility with previous versions
|
||||
```
|
||||
|
||||
Visual studio code configurations:
|
||||
- cvat.js debug starts debugging with entrypoint api.js
|
||||
- cvat.js test builds library and runs entrypoint tests.js
|
||||
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:false
|
||||
*/
|
||||
|
||||
const { defaults } = require('jest-config');
|
||||
|
||||
module.exports = {
|
||||
coverageDirectory: 'reports/coverage',
|
||||
coverageReporters: ['lcov'],
|
||||
moduleFileExtensions: [
|
||||
...defaults.moduleFileExtensions,
|
||||
'ts',
|
||||
'tsx',
|
||||
],
|
||||
reporters: [
|
||||
'default',
|
||||
['jest-junit', { outputDirectory: 'reports/junit' }],
|
||||
],
|
||||
testMatch: [
|
||||
'**/tests/**/*.js',
|
||||
],
|
||||
testPathIgnorePatterns: [
|
||||
'/node_modules/',
|
||||
'/tests/mocks/*',
|
||||
],
|
||||
automock: false,
|
||||
};
|
||||
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
|
||||
module.exports = {
|
||||
plugins: [],
|
||||
recurseDepth: 10,
|
||||
source: {
|
||||
includePattern: '.+\\.js(doc|x)?$',
|
||||
excludePattern: '(^|\\/|\\\\)_',
|
||||
},
|
||||
sourceType: 'module',
|
||||
tags: {
|
||||
allowUnknownTags: false,
|
||||
dictionaries: ['jsdoc', 'closure'],
|
||||
},
|
||||
templates: {
|
||||
cleverLinks: false,
|
||||
monospaceLinks: false,
|
||||
default: {
|
||||
outputSourceFiles: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "cvat-core.js",
|
||||
"version": "0.1.0",
|
||||
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
|
||||
"main": "babel.config.js",
|
||||
"scripts": {
|
||||
"build": "webpack",
|
||||
"test": "jest --config=jest.config.js --coverage",
|
||||
"docs": "jsdoc --readme README.md src/*.js -p -c jsdoc.config.js -d docs",
|
||||
"coveralls": "cat ./reports/coverage/lcov.info | coveralls"
|
||||
},
|
||||
"author": "Intel",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.4.4",
|
||||
"@babel/core": "^7.4.4",
|
||||
"@babel/preset-env": "^7.4.4",
|
||||
"airbnb": "0.0.2",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-loader": "^8.0.6",
|
||||
"core-js": "^3.0.1",
|
||||
"coveralls": "^3.0.5",
|
||||
"eslint": "6.1.0",
|
||||
"eslint-config-airbnb-base": "14.0.0",
|
||||
"eslint-plugin-import": "2.18.2",
|
||||
"eslint-plugin-no-unsafe-innerhtml": "^1.0.16",
|
||||
"eslint-plugin-no-unsanitized": "^3.0.2",
|
||||
"eslint-plugin-security": "^1.4.0",
|
||||
"jest": "^24.8.0",
|
||||
"jest-junit": "^6.4.0",
|
||||
"jsdoc": "^3.6.2",
|
||||
"webpack": "^4.31.0",
|
||||
"webpack-cli": "^3.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.18.0",
|
||||
"browser-or-node": "^1.2.1",
|
||||
"error-stack-parser": "^2.0.2",
|
||||
"form-data": "^2.5.0",
|
||||
"jest-config": "^24.8.0",
|
||||
"js-cookie": "^2.2.0",
|
||||
"platform": "^1.3.5",
|
||||
"store": "^2.0.12"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,235 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
(() => {
|
||||
/**
|
||||
* Class representing an annotation loader
|
||||
* @memberof module:API.cvat.classes
|
||||
* @hideconstructor
|
||||
*/
|
||||
class Loader {
|
||||
constructor(initialData) {
|
||||
const data = {
|
||||
display_name: initialData.display_name,
|
||||
format: initialData.format,
|
||||
handler: initialData.handler,
|
||||
version: initialData.version,
|
||||
};
|
||||
|
||||
Object.defineProperties(this, {
|
||||
name: {
|
||||
/**
|
||||
* @name name
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.Loader
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.display_name,
|
||||
},
|
||||
format: {
|
||||
/**
|
||||
* @name format
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.Loader
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.format,
|
||||
},
|
||||
handler: {
|
||||
/**
|
||||
* @name handler
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.Loader
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.handler,
|
||||
},
|
||||
version: {
|
||||
/**
|
||||
* @name version
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.Loader
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.version,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class representing an annotation dumper
|
||||
* @memberof module:API.cvat.classes
|
||||
* @hideconstructor
|
||||
*/
|
||||
class Dumper {
|
||||
constructor(initialData) {
|
||||
const data = {
|
||||
display_name: initialData.display_name,
|
||||
format: initialData.format,
|
||||
handler: initialData.handler,
|
||||
version: initialData.version,
|
||||
};
|
||||
|
||||
Object.defineProperties(this, {
|
||||
name: {
|
||||
/**
|
||||
* @name name
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.Dumper
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.display_name,
|
||||
},
|
||||
format: {
|
||||
/**
|
||||
* @name format
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.Dumper
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.format,
|
||||
},
|
||||
handler: {
|
||||
/**
|
||||
* @name handler
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.Dumper
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.handler,
|
||||
},
|
||||
version: {
|
||||
/**
|
||||
* @name version
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.Dumper
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.version,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class representing an annotation format
|
||||
* @memberof module:API.cvat.classes
|
||||
* @hideconstructor
|
||||
*/
|
||||
class AnnotationFormat {
|
||||
constructor(initialData) {
|
||||
const data = {
|
||||
created_date: initialData.created_date,
|
||||
updated_date: initialData.updated_date,
|
||||
id: initialData.id,
|
||||
owner: initialData.owner,
|
||||
name: initialData.name,
|
||||
handler_file: initialData.handler_file,
|
||||
};
|
||||
|
||||
data.dumpers = initialData.dumpers.map(el => new Dumper(el));
|
||||
data.loaders = initialData.loaders.map(el => new Loader(el));
|
||||
|
||||
// Now all fields are readonly
|
||||
Object.defineProperties(this, {
|
||||
id: {
|
||||
/**
|
||||
* @name id
|
||||
* @type {integer}
|
||||
* @memberof module:API.cvat.classes.AnnotationFormat
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.id,
|
||||
},
|
||||
owner: {
|
||||
/**
|
||||
* @name owner
|
||||
* @type {integer}
|
||||
* @memberof module:API.cvat.classes.AnnotationFormat
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.owner,
|
||||
},
|
||||
name: {
|
||||
/**
|
||||
* @name name
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.AnnotationFormat
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.name,
|
||||
},
|
||||
createdDate: {
|
||||
/**
|
||||
* @name createdDate
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.AnnotationFormat
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.created_date,
|
||||
},
|
||||
updatedDate: {
|
||||
/**
|
||||
* @name updatedDate
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.AnnotationFormat
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.updated_date,
|
||||
},
|
||||
handlerFile: {
|
||||
/**
|
||||
* @name handlerFile
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.AnnotationFormat
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.handler_file,
|
||||
},
|
||||
loaders: {
|
||||
/**
|
||||
* @name loaders
|
||||
* @type {module:API.cvat.classes.Loader[]}
|
||||
* @memberof module:API.cvat.classes.AnnotationFormat
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => [...data.loaders],
|
||||
},
|
||||
dumpers: {
|
||||
/**
|
||||
* @name dumpers
|
||||
* @type {module:API.cvat.classes.Dumper[]}
|
||||
* @memberof module:API.cvat.classes.AnnotationFormat
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => [...data.dumpers],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
AnnotationFormat,
|
||||
Loader,
|
||||
Dumper,
|
||||
};
|
||||
})();
|
||||
@ -0,0 +1,763 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:false
|
||||
*/
|
||||
|
||||
(() => {
|
||||
const {
|
||||
RectangleShape,
|
||||
PolygonShape,
|
||||
PolylineShape,
|
||||
PointsShape,
|
||||
RectangleTrack,
|
||||
PolygonTrack,
|
||||
PolylineTrack,
|
||||
PointsTrack,
|
||||
Track,
|
||||
Shape,
|
||||
Tag,
|
||||
objectStateFactory,
|
||||
} = require('./annotations-objects');
|
||||
const { checkObjectType } = require('./common');
|
||||
const Statistics = require('./statistics');
|
||||
const { Label } = require('./labels');
|
||||
const {
|
||||
DataError,
|
||||
ArgumentError,
|
||||
ScriptingError,
|
||||
} = require('./exceptions');
|
||||
|
||||
const {
|
||||
ObjectShape,
|
||||
ObjectType,
|
||||
} = require('./enums');
|
||||
const ObjectState = require('./object-state');
|
||||
|
||||
const colors = [
|
||||
'#0066FF', '#AF593E', '#01A368', '#FF861F', '#ED0A3F', '#FF3F34', '#76D7EA',
|
||||
'#8359A3', '#FBE870', '#C5E17A', '#03BB85', '#FFDF00', '#8B8680', '#0A6B0D',
|
||||
'#8FD8D8', '#A36F40', '#F653A6', '#CA3435', '#FFCBA4', '#FF99CC', '#FA9D5A',
|
||||
'#FFAE42', '#A78B00', '#788193', '#514E49', '#1164B4', '#F4FA9F', '#FED8B1',
|
||||
'#C32148', '#01796F', '#E90067', '#FF91A4', '#404E5A', '#6CDAE7', '#FFC1CC',
|
||||
'#006A93', '#867200', '#E2B631', '#6EEB6E', '#FFC800', '#CC99BA', '#FF007C',
|
||||
'#BC6CAC', '#DCCCD7', '#EBE1C2', '#A6AAAE', '#B99685', '#0086A7', '#5E4330',
|
||||
'#C8A2C8', '#708EB3', '#BC8777', '#B2592D', '#497E48', '#6A2963', '#E6335F',
|
||||
'#00755E', '#B5A895', '#0048ba', '#EED9C4', '#C88A65', '#FF6E4A', '#87421F',
|
||||
'#B2BEB5', '#926F5B', '#00B9FB', '#6456B7', '#DB5079', '#C62D42', '#FA9C44',
|
||||
'#DA8A67', '#FD7C6E', '#93CCEA', '#FCF686', '#503E32', '#FF5470', '#9DE093',
|
||||
'#FF7A00', '#4F69C6', '#A50B5E', '#F0E68C', '#FDFF00', '#F091A9', '#FFFF66',
|
||||
'#6F9940', '#FC74FD', '#652DC1', '#D6AEDD', '#EE34D2', '#BB3385', '#6B3FA0',
|
||||
'#33CC99', '#FFDB00', '#87FF2A', '#6EEB6E', '#FFC800', '#CC99BA', '#7A89B8',
|
||||
'#006A93', '#867200', '#E2B631', '#D9D6CF',
|
||||
];
|
||||
|
||||
function shapeFactory(shapeData, clientID, injection) {
|
||||
const { type } = shapeData;
|
||||
const color = colors[clientID % colors.length];
|
||||
|
||||
let shapeModel = null;
|
||||
switch (type) {
|
||||
case 'rectangle':
|
||||
shapeModel = new RectangleShape(shapeData, clientID, color, injection);
|
||||
break;
|
||||
case 'polygon':
|
||||
shapeModel = new PolygonShape(shapeData, clientID, color, injection);
|
||||
break;
|
||||
case 'polyline':
|
||||
shapeModel = new PolylineShape(shapeData, clientID, color, injection);
|
||||
break;
|
||||
case 'points':
|
||||
shapeModel = new PointsShape(shapeData, clientID, color, injection);
|
||||
break;
|
||||
default:
|
||||
throw new DataError(
|
||||
`An unexpected type of shape "${type}"`,
|
||||
);
|
||||
}
|
||||
|
||||
return shapeModel;
|
||||
}
|
||||
|
||||
|
||||
function trackFactory(trackData, clientID, injection) {
|
||||
if (trackData.shapes.length) {
|
||||
const { type } = trackData.shapes[0];
|
||||
const color = colors[clientID % colors.length];
|
||||
|
||||
let trackModel = null;
|
||||
switch (type) {
|
||||
case 'rectangle':
|
||||
trackModel = new RectangleTrack(trackData, clientID, color, injection);
|
||||
break;
|
||||
case 'polygon':
|
||||
trackModel = new PolygonTrack(trackData, clientID, color, injection);
|
||||
break;
|
||||
case 'polyline':
|
||||
trackModel = new PolylineTrack(trackData, clientID, color, injection);
|
||||
break;
|
||||
case 'points':
|
||||
trackModel = new PointsTrack(trackData, clientID, color, injection);
|
||||
break;
|
||||
default:
|
||||
throw new DataError(
|
||||
`An unexpected type of track "${type}"`,
|
||||
);
|
||||
}
|
||||
|
||||
return trackModel;
|
||||
}
|
||||
|
||||
console.warn('The track without any shapes had been found. It was ignored.');
|
||||
return null;
|
||||
}
|
||||
|
||||
class Collection {
|
||||
constructor(data) {
|
||||
this.startFrame = data.startFrame;
|
||||
this.stopFrame = data.stopFrame;
|
||||
this.frameMeta = data.frameMeta;
|
||||
|
||||
this.labels = data.labels.reduce((labelAccumulator, label) => {
|
||||
labelAccumulator[label.id] = label;
|
||||
return labelAccumulator;
|
||||
}, {});
|
||||
|
||||
this.shapes = {}; // key is a frame
|
||||
this.tags = {}; // key is a frame
|
||||
this.tracks = [];
|
||||
this.objects = {}; // key is a client id
|
||||
this.count = 0;
|
||||
this.flush = false;
|
||||
this.collectionZ = {}; // key is a frame, {max, min} are values
|
||||
this.groups = {
|
||||
max: 0,
|
||||
}; // it is an object to we can pass it as an argument by a reference
|
||||
this.injection = {
|
||||
labels: this.labels,
|
||||
collectionZ: this.collectionZ,
|
||||
groups: this.groups,
|
||||
frameMeta: this.frameMeta,
|
||||
};
|
||||
}
|
||||
|
||||
import(data) {
|
||||
for (const tag of data.tags) {
|
||||
const clientID = ++this.count;
|
||||
const tagModel = new Tag(tag, clientID, this.injection);
|
||||
this.tags[tagModel.frame] = this.tags[tagModel.frame] || [];
|
||||
this.tags[tagModel.frame].push(tagModel);
|
||||
this.objects[clientID] = tagModel;
|
||||
}
|
||||
|
||||
for (const shape of data.shapes) {
|
||||
const clientID = ++this.count;
|
||||
const shapeModel = shapeFactory(shape, clientID, this.injection);
|
||||
this.shapes[shapeModel.frame] = this.shapes[shapeModel.frame] || [];
|
||||
this.shapes[shapeModel.frame].push(shapeModel);
|
||||
this.objects[clientID] = shapeModel;
|
||||
}
|
||||
|
||||
for (const track of data.tracks) {
|
||||
const clientID = ++this.count;
|
||||
const trackModel = trackFactory(track, clientID, this.injection);
|
||||
// The function can return null if track doesn't have any shapes.
|
||||
// In this case a corresponded message will be sent to the console
|
||||
if (trackModel) {
|
||||
this.tracks.push(trackModel);
|
||||
this.objects[clientID] = trackModel;
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
export() {
|
||||
const data = {
|
||||
tracks: this.tracks.filter(track => !track.removed)
|
||||
.map(track => track.toJSON()),
|
||||
shapes: Object.values(this.shapes)
|
||||
.reduce((accumulator, value) => {
|
||||
accumulator.push(...value);
|
||||
return accumulator;
|
||||
}, []).filter(shape => !shape.removed)
|
||||
.map(shape => shape.toJSON()),
|
||||
tags: Object.values(this.tags).reduce((accumulator, value) => {
|
||||
accumulator.push(...value);
|
||||
return accumulator;
|
||||
}, []).filter(tag => !tag.removed)
|
||||
.map(tag => tag.toJSON()),
|
||||
};
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
get(frame) {
|
||||
const { tracks } = this;
|
||||
const shapes = this.shapes[frame] || [];
|
||||
const tags = this.tags[frame] || [];
|
||||
|
||||
const objects = tracks.concat(shapes).concat(tags).filter(object => !object.removed);
|
||||
// filtering here
|
||||
|
||||
const objectStates = [];
|
||||
for (const object of objects) {
|
||||
const stateData = object.get(frame);
|
||||
if (stateData.outside && !stateData.keyframe) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const objectState = objectStateFactory.call(object, frame, stateData);
|
||||
objectStates.push(objectState);
|
||||
}
|
||||
|
||||
return objectStates;
|
||||
}
|
||||
|
||||
merge(objectStates) {
|
||||
checkObjectType('shapes for merge', objectStates, null, Array);
|
||||
if (!objectStates.length) return;
|
||||
const objectsForMerge = objectStates.map((state) => {
|
||||
checkObjectType('object state', state, null, ObjectState);
|
||||
const object = this.objects[state.clientID];
|
||||
if (typeof (object) === 'undefined') {
|
||||
throw new ArgumentError(
|
||||
'The object has not been saved yet. Call ObjectState.put([state]) before you can merge it',
|
||||
);
|
||||
}
|
||||
return object;
|
||||
});
|
||||
|
||||
const keyframes = {}; // frame: position
|
||||
const { label, shapeType } = objectStates[0];
|
||||
if (!(label.id in this.labels)) {
|
||||
throw new ArgumentError(
|
||||
`Unknown label for the task: ${label.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!Object.values(ObjectShape).includes(shapeType)) {
|
||||
throw new ArgumentError(
|
||||
`Got unknown shapeType "${shapeType}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const labelAttributes = label.attributes.reduce((accumulator, attribute) => {
|
||||
accumulator[attribute.id] = attribute;
|
||||
return accumulator;
|
||||
}, {});
|
||||
|
||||
for (let i = 0; i < objectsForMerge.length; i++) {
|
||||
// For each state get corresponding object
|
||||
const object = objectsForMerge[i];
|
||||
const state = objectStates[i];
|
||||
if (state.label.id !== label.id) {
|
||||
throw new ArgumentError(
|
||||
`All shape labels are expected to be ${label.name}, but got ${state.label.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (state.shapeType !== shapeType) {
|
||||
throw new ArgumentError(
|
||||
`All shapes are expected to be ${shapeType}, but got ${state.shapeType}`,
|
||||
);
|
||||
}
|
||||
|
||||
// If this object is shape, get it position and save as a keyframe
|
||||
if (object instanceof Shape) {
|
||||
// Frame already saved and it is not outside
|
||||
if (object.frame in keyframes && !keyframes[object.frame].outside) {
|
||||
throw new ArgumentError(
|
||||
'Expected only one visible shape per frame',
|
||||
);
|
||||
}
|
||||
|
||||
keyframes[object.frame] = {
|
||||
type: shapeType,
|
||||
frame: object.frame,
|
||||
points: [...object.points],
|
||||
occluded: object.occluded,
|
||||
zOrder: object.zOrder,
|
||||
outside: false,
|
||||
attributes: Object.keys(object.attributes).reduce((accumulator, attrID) => {
|
||||
// We save only mutable attributes inside a keyframe
|
||||
if (attrID in labelAttributes && labelAttributes[attrID].mutable) {
|
||||
accumulator.push({
|
||||
spec_id: +attrID,
|
||||
value: object.attributes[attrID],
|
||||
});
|
||||
}
|
||||
return accumulator;
|
||||
}, []),
|
||||
};
|
||||
|
||||
// Push outside shape after each annotation shape
|
||||
// Any not outside shape rewrites it
|
||||
if (!((object.frame + 1) in keyframes)) {
|
||||
keyframes[object.frame + 1] = JSON
|
||||
.parse(JSON.stringify(keyframes[object.frame]));
|
||||
keyframes[object.frame + 1].outside = true;
|
||||
keyframes[object.frame + 1].frame++;
|
||||
}
|
||||
} else if (object instanceof Track) {
|
||||
// If this object is track, iterate through all its
|
||||
// keyframes and push copies to new keyframes
|
||||
const attributes = {}; // id:value
|
||||
for (const keyframe of Object.keys(object.shapes)) {
|
||||
const shape = object.shapes[keyframe];
|
||||
// Frame already saved and it is not outside
|
||||
if (keyframe in keyframes && !keyframes[keyframe].outside) {
|
||||
// This shape is outside and non-outside shape already exists
|
||||
if (shape.outside) {
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new ArgumentError(
|
||||
'Expected only one visible shape per frame',
|
||||
);
|
||||
}
|
||||
|
||||
// We do not save an attribute if it has the same value
|
||||
// We save only updates
|
||||
let updatedAttributes = false;
|
||||
for (const attrID in shape.attributes) {
|
||||
if (!(attrID in attributes)
|
||||
|| attributes[attrID] !== shape.attributes[attrID]) {
|
||||
updatedAttributes = true;
|
||||
attributes[attrID] = shape.attributes[attrID];
|
||||
}
|
||||
}
|
||||
|
||||
keyframes[keyframe] = {
|
||||
type: shapeType,
|
||||
frame: +keyframe,
|
||||
points: [...shape.points],
|
||||
occluded: shape.occluded,
|
||||
outside: shape.outside,
|
||||
zOrder: shape.zOrder,
|
||||
attributes: updatedAttributes ? Object.keys(attributes)
|
||||
.reduce((accumulator, attrID) => {
|
||||
accumulator.push({
|
||||
spec_id: +attrID,
|
||||
value: attributes[attrID],
|
||||
});
|
||||
|
||||
return accumulator;
|
||||
}, []) : [],
|
||||
};
|
||||
}
|
||||
} else {
|
||||
throw new ArgumentError(
|
||||
`Trying to merge unknown object type: ${object.constructor.name}. `
|
||||
+ 'Only shapes and tracks are expected.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let firstNonOutside = false;
|
||||
for (const frame of Object.keys(keyframes).sort((a, b) => +a - +b)) {
|
||||
// Remove all outside frames at the begin
|
||||
firstNonOutside = firstNonOutside || keyframes[frame].outside;
|
||||
if (!firstNonOutside && keyframes[frame].outside) {
|
||||
delete keyframes[frame];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const clientID = ++this.count;
|
||||
const track = {
|
||||
frame: Math.min.apply(null, Object.keys(keyframes).map(frame => +frame)),
|
||||
shapes: Object.values(keyframes),
|
||||
group: 0,
|
||||
label_id: label.id,
|
||||
attributes: Object.keys(objectStates[0].attributes)
|
||||
.reduce((accumulator, attrID) => {
|
||||
if (!labelAttributes[attrID].mutable) {
|
||||
accumulator.push({
|
||||
spec_id: +attrID,
|
||||
value: objectStates[0].attributes[attrID],
|
||||
});
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
}, []),
|
||||
};
|
||||
|
||||
const trackModel = trackFactory(track, clientID, this.injection);
|
||||
this.tracks.push(trackModel);
|
||||
this.objects[clientID] = trackModel;
|
||||
|
||||
// Remove other shapes
|
||||
for (const object of objectsForMerge) {
|
||||
object.removed = true;
|
||||
if (typeof (object.resetCache) === 'function') {
|
||||
object.resetCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
split(objectState, frame) {
|
||||
checkObjectType('object state', objectState, null, ObjectState);
|
||||
checkObjectType('frame', frame, 'integer', null);
|
||||
|
||||
const object = this.objects[objectState.clientID];
|
||||
if (typeof (object) === 'undefined') {
|
||||
throw new ArgumentError(
|
||||
'The object has not been saved yet. Call annotations.put([state]) before',
|
||||
);
|
||||
}
|
||||
|
||||
if (objectState.objectType !== ObjectType.TRACK) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keyframes = Object.keys(object.shapes).sort((a, b) => +a - +b);
|
||||
if (frame <= +keyframes[0] || frame > keyframes[keyframes.length - 1]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const labelAttributes = object.label.attributes.reduce((accumulator, attribute) => {
|
||||
accumulator[attribute.id] = attribute;
|
||||
return accumulator;
|
||||
}, {});
|
||||
|
||||
const exported = object.toJSON();
|
||||
const position = {
|
||||
type: objectState.shapeType,
|
||||
points: [...objectState.points],
|
||||
occluded: objectState.occluded,
|
||||
outside: objectState.outside,
|
||||
zOrder: 0,
|
||||
attributes: Object.keys(objectState.attributes)
|
||||
.reduce((accumulator, attrID) => {
|
||||
if (!labelAttributes[attrID].mutable) {
|
||||
accumulator.push({
|
||||
spec_id: +attrID,
|
||||
value: objectState.attributes[attrID],
|
||||
});
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
}, []),
|
||||
frame,
|
||||
};
|
||||
|
||||
const prev = {
|
||||
frame: exported.frame,
|
||||
group: 0,
|
||||
label_id: exported.label_id,
|
||||
attributes: exported.attributes,
|
||||
shapes: [],
|
||||
};
|
||||
|
||||
const next = JSON.parse(JSON.stringify(prev));
|
||||
next.frame = frame;
|
||||
|
||||
next.shapes.push(JSON.parse(JSON.stringify(position)));
|
||||
exported.shapes.map((shape) => {
|
||||
delete shape.id;
|
||||
if (shape.frame < frame) {
|
||||
prev.shapes.push(JSON.parse(JSON.stringify(shape)));
|
||||
} else if (shape.frame > frame) {
|
||||
next.shapes.push(JSON.parse(JSON.stringify(shape)));
|
||||
}
|
||||
|
||||
return shape;
|
||||
});
|
||||
prev.shapes.push(position);
|
||||
prev.shapes[prev.shapes.length - 1].outside = true;
|
||||
|
||||
let clientID = ++this.count;
|
||||
const prevTrack = trackFactory(prev, clientID, this.injection);
|
||||
this.tracks.push(prevTrack);
|
||||
this.objects[clientID] = prevTrack;
|
||||
|
||||
clientID = ++this.count;
|
||||
const nextTrack = trackFactory(next, clientID, this.injection);
|
||||
this.tracks.push(nextTrack);
|
||||
this.objects[clientID] = nextTrack;
|
||||
|
||||
// Remove source object
|
||||
object.removed = true;
|
||||
object.resetCache();
|
||||
}
|
||||
|
||||
group(objectStates, reset) {
|
||||
checkObjectType('shapes for group', objectStates, null, Array);
|
||||
|
||||
const objectsForGroup = objectStates.map((state) => {
|
||||
checkObjectType('object state', state, null, ObjectState);
|
||||
const object = this.objects[state.clientID];
|
||||
if (typeof (object) === 'undefined') {
|
||||
throw new ArgumentError(
|
||||
'The object has not been saved yet. Call annotations.put([state]) before',
|
||||
);
|
||||
}
|
||||
return object;
|
||||
});
|
||||
|
||||
const groupIdx = reset ? 0 : ++this.groups.max;
|
||||
for (const object of objectsForGroup) {
|
||||
object.group = groupIdx;
|
||||
if (typeof (object.resetCache) === 'function') {
|
||||
object.resetCache();
|
||||
}
|
||||
}
|
||||
|
||||
return groupIdx;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.shapes = {};
|
||||
this.tags = {};
|
||||
this.tracks = [];
|
||||
this.objects = {}; // by id
|
||||
this.count = 0;
|
||||
|
||||
this.flush = true;
|
||||
}
|
||||
|
||||
statistics() {
|
||||
const labels = {};
|
||||
const skeleton = {
|
||||
rectangle: {
|
||||
shape: 0,
|
||||
track: 0,
|
||||
},
|
||||
polygon: {
|
||||
shape: 0,
|
||||
track: 0,
|
||||
},
|
||||
polyline: {
|
||||
shape: 0,
|
||||
track: 0,
|
||||
},
|
||||
points: {
|
||||
shape: 0,
|
||||
track: 0,
|
||||
},
|
||||
tags: 0,
|
||||
manually: 0,
|
||||
interpolated: 0,
|
||||
total: 0,
|
||||
};
|
||||
|
||||
const total = JSON.parse(JSON.stringify(skeleton));
|
||||
for (const label of Object.values(this.labels)) {
|
||||
const { name } = label;
|
||||
labels[name] = JSON.parse(JSON.stringify(skeleton));
|
||||
}
|
||||
|
||||
for (const object of Object.values(this.objects)) {
|
||||
let objectType = null;
|
||||
if (object instanceof Shape) {
|
||||
objectType = 'shape';
|
||||
} else if (object instanceof Track) {
|
||||
objectType = 'track';
|
||||
} else if (object instanceof Tag) {
|
||||
objectType = 'tag';
|
||||
} else {
|
||||
throw new ScriptingError(
|
||||
`Unexpected object type: "${objectType}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const label = object.label.name;
|
||||
if (objectType === 'tag') {
|
||||
labels[label].tags++;
|
||||
labels[label].manually++;
|
||||
labels[label].total++;
|
||||
} else {
|
||||
const { shapeType } = object;
|
||||
labels[label][shapeType][objectType]++;
|
||||
|
||||
if (objectType === 'track') {
|
||||
const keyframes = Object.keys(object.shapes)
|
||||
.sort((a, b) => +a - +b).map(el => +el);
|
||||
|
||||
let prevKeyframe = keyframes[0];
|
||||
let visible = false;
|
||||
|
||||
for (const keyframe of keyframes) {
|
||||
if (visible) {
|
||||
const interpolated = keyframe - prevKeyframe - 1;
|
||||
labels[label].interpolated += interpolated;
|
||||
labels[label].total += interpolated;
|
||||
}
|
||||
visible = !object.shapes[keyframe].outside;
|
||||
prevKeyframe = keyframe;
|
||||
|
||||
if (visible) {
|
||||
labels[label].manually++;
|
||||
labels[label].total++;
|
||||
}
|
||||
}
|
||||
|
||||
const lastKey = keyframes[keyframes.length - 1];
|
||||
if (lastKey !== this.stopFrame && !object.shapes[lastKey].outside) {
|
||||
const interpolated = this.stopFrame - lastKey;
|
||||
labels[label].interpolated += interpolated;
|
||||
labels[label].total += interpolated;
|
||||
}
|
||||
} else {
|
||||
labels[label].manually++;
|
||||
labels[label].total++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const label of Object.keys(labels)) {
|
||||
for (const key of Object.keys(labels[label])) {
|
||||
if (typeof (labels[label][key]) === 'object') {
|
||||
for (const objectType of Object.keys(labels[label][key])) {
|
||||
total[key][objectType] += labels[label][key][objectType];
|
||||
}
|
||||
} else {
|
||||
total[key] += labels[label][key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Statistics(labels, total);
|
||||
}
|
||||
|
||||
put(objectStates) {
|
||||
checkObjectType('shapes for put', objectStates, null, Array);
|
||||
const constructed = {
|
||||
shapes: [],
|
||||
tracks: [],
|
||||
tags: [],
|
||||
};
|
||||
|
||||
function convertAttributes(accumulator, attrID) {
|
||||
const specID = +attrID;
|
||||
const value = this.attributes[attrID];
|
||||
|
||||
checkObjectType('attribute id', specID, 'integer', null);
|
||||
checkObjectType('attribute value', value, 'string', null);
|
||||
|
||||
accumulator.push({
|
||||
spec_id: specID,
|
||||
value,
|
||||
});
|
||||
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
for (const state of objectStates) {
|
||||
checkObjectType('object state', state, null, ObjectState);
|
||||
checkObjectType('state client ID', state.clientID, 'undefined', null);
|
||||
checkObjectType('state frame', state.frame, 'integer', null);
|
||||
checkObjectType('state attributes', state.attributes, null, Object);
|
||||
checkObjectType('state label', state.label, null, Label);
|
||||
|
||||
const attributes = Object.keys(state.attributes)
|
||||
.reduce(convertAttributes.bind(state), []);
|
||||
const labelAttributes = state.label.attributes.reduce((accumulator, attribute) => {
|
||||
accumulator[attribute.id] = attribute;
|
||||
return accumulator;
|
||||
}, {});
|
||||
|
||||
// Construct whole objects from states
|
||||
if (state.objectType === 'tag') {
|
||||
constructed.tags.push({
|
||||
attributes,
|
||||
frame: state.frame,
|
||||
label_id: state.label.id,
|
||||
group: 0,
|
||||
});
|
||||
} else {
|
||||
checkObjectType('state occluded', state.occluded, 'boolean', null);
|
||||
checkObjectType('state points', state.points, null, Array);
|
||||
|
||||
for (const coord of state.points) {
|
||||
checkObjectType('point coordinate', coord, 'number', null);
|
||||
}
|
||||
|
||||
if (!Object.values(ObjectShape).includes(state.shapeType)) {
|
||||
throw new ArgumentError(
|
||||
'Object shape must be one of: '
|
||||
+ `${JSON.stringify(Object.values(ObjectShape))}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (state.objectType === 'shape') {
|
||||
constructed.shapes.push({
|
||||
attributes,
|
||||
frame: state.frame,
|
||||
group: 0,
|
||||
label_id: state.label.id,
|
||||
occluded: state.occluded || false,
|
||||
points: [...state.points],
|
||||
type: state.shapeType,
|
||||
z_order: 0,
|
||||
});
|
||||
} else if (state.objectType === 'track') {
|
||||
constructed.tracks.push({
|
||||
attributes: attributes
|
||||
.filter(attr => !labelAttributes[attr.spec_id].mutable),
|
||||
frame: state.frame,
|
||||
group: 0,
|
||||
label_id: state.label.id,
|
||||
shapes: [{
|
||||
attributes: attributes
|
||||
.filter(attr => labelAttributes[attr.spec_id].mutable),
|
||||
frame: state.frame,
|
||||
occluded: state.occluded || false,
|
||||
outside: false,
|
||||
points: [...state.points],
|
||||
type: state.shapeType,
|
||||
z_order: 0,
|
||||
}],
|
||||
});
|
||||
} else {
|
||||
throw new ArgumentError(
|
||||
'Object type must be one of: '
|
||||
+ `${JSON.stringify(Object.values(ObjectType))}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add constructed objects to a collection
|
||||
this.import(constructed);
|
||||
}
|
||||
|
||||
select(objectStates, x, y) {
|
||||
checkObjectType('shapes for select', objectStates, null, Array);
|
||||
checkObjectType('x coordinate', x, 'number', null);
|
||||
checkObjectType('y coordinate', y, 'number', null);
|
||||
|
||||
let minimumDistance = null;
|
||||
let minimumState = null;
|
||||
for (const state of objectStates) {
|
||||
checkObjectType('object state', state, null, ObjectState);
|
||||
if (state.outside) continue;
|
||||
|
||||
const object = this.objects[state.clientID];
|
||||
if (typeof (object) === 'undefined') {
|
||||
throw new ArgumentError(
|
||||
'The object has not been saved yet. Call annotations.put([state]) before',
|
||||
);
|
||||
}
|
||||
|
||||
const distance = object.constructor.distance(state.points, x, y);
|
||||
if (distance !== null && (minimumDistance === null || distance < minimumDistance)) {
|
||||
minimumDistance = distance;
|
||||
minimumState = state;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
state: minimumState,
|
||||
distance: minimumDistance,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Collection;
|
||||
})();
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,279 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:false
|
||||
*/
|
||||
|
||||
(() => {
|
||||
const serverProxy = require('./server-proxy');
|
||||
const { Task } = require('./session');
|
||||
const { ScriptingError } = ('./exceptions');
|
||||
|
||||
class AnnotationsSaver {
|
||||
constructor(version, collection, session) {
|
||||
this.sessionType = session instanceof Task ? 'task' : 'job';
|
||||
this.id = session.id;
|
||||
this.version = version;
|
||||
this.collection = collection;
|
||||
this.initialObjects = {};
|
||||
this.hash = this._getHash();
|
||||
|
||||
// We need use data from export instead of initialData
|
||||
// Otherwise we have differ keys order and JSON comparison code incorrect
|
||||
const exported = this.collection.export();
|
||||
|
||||
this._resetState();
|
||||
for (const shape of exported.shapes) {
|
||||
this.initialObjects.shapes[shape.id] = shape;
|
||||
}
|
||||
|
||||
for (const track of exported.tracks) {
|
||||
this.initialObjects.tracks[track.id] = track;
|
||||
}
|
||||
|
||||
for (const tag of exported.tags) {
|
||||
this.initialObjects.tags[tag.id] = tag;
|
||||
}
|
||||
}
|
||||
|
||||
_resetState() {
|
||||
this.initialObjects = {
|
||||
shapes: {},
|
||||
tracks: {},
|
||||
tags: {},
|
||||
};
|
||||
}
|
||||
|
||||
_getHash() {
|
||||
const exported = this.collection.export();
|
||||
return JSON.stringify(exported);
|
||||
}
|
||||
|
||||
async _request(data, action) {
|
||||
const result = await serverProxy.annotations.updateAnnotations(
|
||||
this.sessionType,
|
||||
this.id,
|
||||
data,
|
||||
action,
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async _put(data) {
|
||||
const result = await this._request(data, 'put');
|
||||
return result;
|
||||
}
|
||||
|
||||
async _create(created) {
|
||||
const result = await this._request(created, 'create');
|
||||
return result;
|
||||
}
|
||||
|
||||
async _update(updated) {
|
||||
const result = await this._request(updated, 'update');
|
||||
return result;
|
||||
}
|
||||
|
||||
async _delete(deleted) {
|
||||
const result = await this._request(deleted, 'delete');
|
||||
return result;
|
||||
}
|
||||
|
||||
_split(exported) {
|
||||
const splitted = {
|
||||
created: {
|
||||
shapes: [],
|
||||
tracks: [],
|
||||
tags: [],
|
||||
},
|
||||
updated: {
|
||||
shapes: [],
|
||||
tracks: [],
|
||||
tags: [],
|
||||
},
|
||||
deleted: {
|
||||
shapes: [],
|
||||
tracks: [],
|
||||
tags: [],
|
||||
},
|
||||
};
|
||||
|
||||
// Find created and updated objects
|
||||
for (const type of Object.keys(exported)) {
|
||||
for (const object of exported[type]) {
|
||||
if (object.id in this.initialObjects[type]) {
|
||||
const exportedHash = JSON.stringify(object);
|
||||
const initialHash = JSON.stringify(this.initialObjects[type][object.id]);
|
||||
if (exportedHash !== initialHash) {
|
||||
splitted.updated[type].push(object);
|
||||
}
|
||||
} else if (typeof (object.id) === 'undefined') {
|
||||
splitted.created[type].push(object);
|
||||
} else {
|
||||
throw new ScriptingError(
|
||||
`Id of object is defined "${object.id}"`
|
||||
+ 'but it absents in initial state',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now find deleted objects
|
||||
const indexes = {
|
||||
shapes: exported.shapes.map((object) => +object.id),
|
||||
tracks: exported.tracks.map((object) => +object.id),
|
||||
tags: exported.tags.map((object) => +object.id),
|
||||
};
|
||||
|
||||
for (const type of Object.keys(this.initialObjects)) {
|
||||
for (const id of Object.keys(this.initialObjects[type])) {
|
||||
if (!indexes[type].includes(+id)) {
|
||||
const object = this.initialObjects[type][id];
|
||||
splitted.deleted[type].push(object);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return splitted;
|
||||
}
|
||||
|
||||
_updateCreatedObjects(saved, indexes) {
|
||||
const savedLength = saved.tracks.length
|
||||
+ saved.shapes.length + saved.tags.length;
|
||||
|
||||
const indexesLength = indexes.tracks.length
|
||||
+ indexes.shapes.length + indexes.tags.length;
|
||||
|
||||
if (indexesLength !== savedLength) {
|
||||
throw new ScriptingError(
|
||||
'Number of indexes is differed by number of saved objects'
|
||||
+ `${indexesLength} vs ${savedLength}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Updated IDs of created objects
|
||||
for (const type of Object.keys(indexes)) {
|
||||
for (let i = 0; i < indexes[type].length; i++) {
|
||||
const clientID = indexes[type][i];
|
||||
this.collection.objects[clientID].serverID = saved[type][i].id;
|
||||
if (type === 'tracks') {
|
||||
// We have to reset cache because of old value of serverID was saved there
|
||||
this.collection.objects[clientID].resetCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_receiveIndexes(exported) {
|
||||
// Receive client indexes before saving
|
||||
const indexes = {
|
||||
tracks: exported.tracks.map((track) => track.clientID),
|
||||
shapes: exported.shapes.map((shape) => shape.clientID),
|
||||
tags: exported.tags.map((tag) => tag.clientID),
|
||||
};
|
||||
|
||||
// Remove them from the request body
|
||||
exported.tracks.concat(exported.shapes).concat(exported.tags)
|
||||
.map((value) => {
|
||||
delete value.clientID;
|
||||
return value;
|
||||
});
|
||||
|
||||
return indexes;
|
||||
}
|
||||
|
||||
async save(onUpdate) {
|
||||
if (typeof onUpdate !== 'function') {
|
||||
onUpdate = (message) => {
|
||||
console.log(message);
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const exported = this.collection.export();
|
||||
const { flush } = this.collection;
|
||||
if (flush) {
|
||||
onUpdate('New objects are being saved..');
|
||||
const indexes = this._receiveIndexes(exported);
|
||||
const savedData = await this._put({ ...exported, version: this.version });
|
||||
this.version = savedData.version;
|
||||
this.collection.flush = false;
|
||||
|
||||
onUpdate('Saved objects are being updated in the client');
|
||||
this._updateCreatedObjects(savedData, indexes);
|
||||
|
||||
onUpdate('Initial state is being updated');
|
||||
|
||||
this._resetState();
|
||||
for (const type of Object.keys(this.initialObjects)) {
|
||||
for (const object of savedData[type]) {
|
||||
this.initialObjects[type][object.id] = object;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const {
|
||||
created,
|
||||
updated,
|
||||
deleted,
|
||||
} = this._split(exported);
|
||||
|
||||
onUpdate('New objects are being saved..');
|
||||
const indexes = this._receiveIndexes(created);
|
||||
const createdData = await this._create({ ...created, version: this.version });
|
||||
this.version = createdData.version;
|
||||
|
||||
onUpdate('Saved objects are being updated in the client');
|
||||
this._updateCreatedObjects(createdData, indexes);
|
||||
|
||||
onUpdate('Initial state is being updated');
|
||||
for (const type of Object.keys(this.initialObjects)) {
|
||||
for (const object of createdData[type]) {
|
||||
this.initialObjects[type][object.id] = object;
|
||||
}
|
||||
}
|
||||
|
||||
onUpdate('Changed objects are being saved..');
|
||||
this._receiveIndexes(updated);
|
||||
const updatedData = await this._update({ ...updated, version: this.version });
|
||||
this.version = updatedData.version;
|
||||
|
||||
onUpdate('Initial state is being updated');
|
||||
for (const type of Object.keys(this.initialObjects)) {
|
||||
for (const object of updatedData[type]) {
|
||||
this.initialObjects[type][object.id] = object;
|
||||
}
|
||||
}
|
||||
|
||||
onUpdate('Changed objects are being saved..');
|
||||
this._receiveIndexes(deleted);
|
||||
const deletedData = await this._delete({ ...deleted, version: this.version });
|
||||
this._version = deletedData.version;
|
||||
|
||||
onUpdate('Initial state is being updated');
|
||||
for (const type of Object.keys(this.initialObjects)) {
|
||||
for (const object of deletedData[type]) {
|
||||
delete this.initialObjects[type][object.id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.hash = this._getHash();
|
||||
onUpdate('Saving is done');
|
||||
} catch (error) {
|
||||
onUpdate(`Can not save annotations: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
hasUnsavedChanges() {
|
||||
return this._getHash() !== this.hash;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AnnotationsSaver;
|
||||
})();
|
||||
@ -0,0 +1,242 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:false
|
||||
*/
|
||||
|
||||
(() => {
|
||||
const serverProxy = require('./server-proxy');
|
||||
const Collection = require('./annotations-collection');
|
||||
const AnnotationsSaver = require('./annotations-saver');
|
||||
const { checkObjectType } = require('./common');
|
||||
const { Task } = require('./session');
|
||||
const {
|
||||
Loader,
|
||||
Dumper,
|
||||
} = require('./annotation-format.js');
|
||||
const {
|
||||
ScriptingError,
|
||||
DataError,
|
||||
ArgumentError,
|
||||
} = require('./exceptions');
|
||||
|
||||
const jobCache = new WeakMap();
|
||||
const taskCache = new WeakMap();
|
||||
|
||||
function getCache(sessionType) {
|
||||
if (sessionType === 'task') {
|
||||
return taskCache;
|
||||
}
|
||||
|
||||
if (sessionType === 'job') {
|
||||
return jobCache;
|
||||
}
|
||||
|
||||
throw new ScriptingError(
|
||||
`Unknown session type was received ${sessionType}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function getAnnotationsFromServer(session) {
|
||||
const sessionType = session instanceof Task ? 'task' : 'job';
|
||||
const cache = getCache(sessionType);
|
||||
|
||||
if (!cache.has(session)) {
|
||||
const rawAnnotations = await serverProxy.annotations
|
||||
.getAnnotations(sessionType, session.id);
|
||||
|
||||
// Get meta information about frames
|
||||
const startFrame = sessionType === 'job' ? session.startFrame : 0;
|
||||
const stopFrame = sessionType === 'job' ? session.stopFrame : session.size - 1;
|
||||
const frameMeta = {};
|
||||
for (let i = startFrame; i <= stopFrame; i++) {
|
||||
frameMeta[i] = await session.frames.get(i);
|
||||
}
|
||||
|
||||
const collection = new Collection({
|
||||
labels: session.labels || session.task.labels,
|
||||
startFrame,
|
||||
stopFrame,
|
||||
frameMeta,
|
||||
}).import(rawAnnotations);
|
||||
|
||||
const saver = new AnnotationsSaver(rawAnnotations.version, collection, session);
|
||||
|
||||
cache.set(session, {
|
||||
collection,
|
||||
saver,
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function getAnnotations(session, frame, filter) {
|
||||
await getAnnotationsFromServer(session);
|
||||
const sessionType = session instanceof Task ? 'task' : 'job';
|
||||
const cache = getCache(sessionType);
|
||||
return cache.get(session).collection.get(frame, filter);
|
||||
}
|
||||
|
||||
async function saveAnnotations(session, onUpdate) {
|
||||
const sessionType = session instanceof Task ? 'task' : 'job';
|
||||
const cache = getCache(sessionType);
|
||||
|
||||
if (cache.has(session)) {
|
||||
await cache.get(session).saver.save(onUpdate);
|
||||
}
|
||||
|
||||
// If a collection wasn't uploaded, than it wasn't changed, finally we shouldn't save it
|
||||
}
|
||||
|
||||
function mergeAnnotations(session, objectStates) {
|
||||
const sessionType = session instanceof Task ? 'task' : 'job';
|
||||
const cache = getCache(sessionType);
|
||||
|
||||
if (cache.has(session)) {
|
||||
return cache.get(session).collection.merge(objectStates);
|
||||
}
|
||||
|
||||
throw new DataError(
|
||||
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
|
||||
);
|
||||
}
|
||||
|
||||
function splitAnnotations(session, objectState, frame) {
|
||||
const sessionType = session instanceof Task ? 'task' : 'job';
|
||||
const cache = getCache(sessionType);
|
||||
|
||||
if (cache.has(session)) {
|
||||
return cache.get(session).collection.split(objectState, frame);
|
||||
}
|
||||
|
||||
throw new DataError(
|
||||
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
|
||||
);
|
||||
}
|
||||
|
||||
function groupAnnotations(session, objectStates, reset) {
|
||||
const sessionType = session instanceof Task ? 'task' : 'job';
|
||||
const cache = getCache(sessionType);
|
||||
|
||||
if (cache.has(session)) {
|
||||
return cache.get(session).collection.group(objectStates, reset);
|
||||
}
|
||||
|
||||
throw new DataError(
|
||||
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
|
||||
);
|
||||
}
|
||||
|
||||
function hasUnsavedChanges(session) {
|
||||
const sessionType = session instanceof Task ? 'task' : 'job';
|
||||
const cache = getCache(sessionType);
|
||||
|
||||
if (cache.has(session)) {
|
||||
return cache.get(session).saver.hasUnsavedChanges();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function clearAnnotations(session, reload) {
|
||||
checkObjectType('reload', reload, 'boolean', null);
|
||||
const sessionType = session instanceof Task ? 'task' : 'job';
|
||||
const cache = getCache(sessionType);
|
||||
|
||||
if (cache.has(session)) {
|
||||
cache.get(session).collection.clear();
|
||||
}
|
||||
|
||||
if (reload) {
|
||||
cache.delete(session);
|
||||
await getAnnotationsFromServer(session);
|
||||
}
|
||||
}
|
||||
|
||||
function annotationsStatistics(session) {
|
||||
const sessionType = session instanceof Task ? 'task' : 'job';
|
||||
const cache = getCache(sessionType);
|
||||
|
||||
if (cache.has(session)) {
|
||||
return cache.get(session).collection.statistics();
|
||||
}
|
||||
|
||||
throw new DataError(
|
||||
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
|
||||
);
|
||||
}
|
||||
|
||||
function putAnnotations(session, objectStates) {
|
||||
const sessionType = session instanceof Task ? 'task' : 'job';
|
||||
const cache = getCache(sessionType);
|
||||
|
||||
if (cache.has(session)) {
|
||||
return cache.get(session).collection.put(objectStates);
|
||||
}
|
||||
|
||||
throw new DataError(
|
||||
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
|
||||
);
|
||||
}
|
||||
|
||||
function selectObject(session, objectStates, x, y) {
|
||||
const sessionType = session instanceof Task ? 'task' : 'job';
|
||||
const cache = getCache(sessionType);
|
||||
|
||||
if (cache.has(session)) {
|
||||
return cache.get(session).collection.select(objectStates, x, y);
|
||||
}
|
||||
|
||||
throw new DataError(
|
||||
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
|
||||
);
|
||||
}
|
||||
|
||||
async function uploadAnnotations(session, file, loader) {
|
||||
const sessionType = session instanceof Task ? 'task' : 'job';
|
||||
if (!(loader instanceof Loader)) {
|
||||
throw new ArgumentError(
|
||||
'A loader must be instance of Loader class',
|
||||
);
|
||||
}
|
||||
await serverProxy.annotations.uploadAnnotations(sessionType, session.id, file, loader.name);
|
||||
}
|
||||
|
||||
async function dumpAnnotations(session, name, dumper) {
|
||||
if (!(dumper instanceof Dumper)) {
|
||||
throw new ArgumentError(
|
||||
'A dumper must be instance of Dumper class',
|
||||
);
|
||||
}
|
||||
|
||||
let result = null;
|
||||
const sessionType = session instanceof Task ? 'task' : 'job';
|
||||
if (sessionType === 'job') {
|
||||
result = await serverProxy.annotations
|
||||
.dumpAnnotations(session.task.id, name, dumper.name);
|
||||
} else {
|
||||
result = await serverProxy.annotations
|
||||
.dumpAnnotations(session.id, name, dumper.name);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAnnotations,
|
||||
putAnnotations,
|
||||
saveAnnotations,
|
||||
hasUnsavedChanges,
|
||||
mergeAnnotations,
|
||||
splitAnnotations,
|
||||
groupAnnotations,
|
||||
clearAnnotations,
|
||||
annotationsStatistics,
|
||||
selectObject,
|
||||
uploadAnnotations,
|
||||
dumpAnnotations,
|
||||
};
|
||||
})();
|
||||
@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* eslint prefer-arrow-callback: [ "error", { "allowNamedFunctions": true } ] */
|
||||
|
||||
/* global
|
||||
require:false
|
||||
*/
|
||||
|
||||
|
||||
(() => {
|
||||
const PluginRegistry = require('./plugins');
|
||||
const serverProxy = require('./server-proxy');
|
||||
const {
|
||||
isBoolean,
|
||||
isInteger,
|
||||
isEnum,
|
||||
isString,
|
||||
checkFilter,
|
||||
} = require('./common');
|
||||
|
||||
const {
|
||||
TaskStatus,
|
||||
TaskMode,
|
||||
} = require('./enums');
|
||||
|
||||
const User = require('./user');
|
||||
const { AnnotationFormat } = require('./annotation-format.js');
|
||||
const { ArgumentError } = require('./exceptions');
|
||||
const { Task } = require('./session');
|
||||
|
||||
function implementAPI(cvat) {
|
||||
cvat.plugins.list.implementation = PluginRegistry.list;
|
||||
cvat.plugins.register.implementation = PluginRegistry.register.bind(cvat);
|
||||
|
||||
cvat.server.about.implementation = async () => {
|
||||
const result = await serverProxy.server.about();
|
||||
return result;
|
||||
};
|
||||
|
||||
cvat.server.share.implementation = async (directory) => {
|
||||
const result = await serverProxy.server.share(directory);
|
||||
return result;
|
||||
};
|
||||
|
||||
cvat.server.formats.implementation = async () => {
|
||||
const result = await serverProxy.server.formats();
|
||||
return result.map(el => new AnnotationFormat(el));
|
||||
};
|
||||
|
||||
cvat.server.register.implementation = async (username, firstName, lastName,
|
||||
email, password1, password2) => {
|
||||
await serverProxy.server.register(username, firstName, lastName, email,
|
||||
password1, password2);
|
||||
};
|
||||
|
||||
cvat.server.login.implementation = async (username, password) => {
|
||||
await serverProxy.server.login(username, password);
|
||||
};
|
||||
|
||||
cvat.server.logout.implementation = async () => {
|
||||
await serverProxy.server.logout();
|
||||
};
|
||||
|
||||
cvat.server.authorized.implementation = async () => {
|
||||
const result = await serverProxy.server.authorized();
|
||||
return result;
|
||||
};
|
||||
|
||||
cvat.users.get.implementation = async (filter) => {
|
||||
checkFilter(filter, {
|
||||
self: isBoolean,
|
||||
});
|
||||
|
||||
let users = null;
|
||||
if ('self' in filter && filter.self) {
|
||||
users = await serverProxy.users.getSelf();
|
||||
users = [users];
|
||||
} else {
|
||||
users = await serverProxy.users.getUsers();
|
||||
}
|
||||
|
||||
users = users.map(user => new User(user));
|
||||
return users;
|
||||
};
|
||||
|
||||
cvat.jobs.get.implementation = async (filter) => {
|
||||
checkFilter(filter, {
|
||||
taskID: isInteger,
|
||||
jobID: isInteger,
|
||||
});
|
||||
|
||||
if (('taskID' in filter) && ('jobID' in filter)) {
|
||||
throw new ArgumentError(
|
||||
'Only one of fields "taskID" and "jobID" allowed simultaneously',
|
||||
);
|
||||
}
|
||||
|
||||
if (!Object.keys(filter).length) {
|
||||
throw new ArgumentError(
|
||||
'Job filter must not be empty',
|
||||
);
|
||||
}
|
||||
|
||||
let tasks = null;
|
||||
if ('taskID' in filter) {
|
||||
tasks = await serverProxy.tasks.getTasks(`id=${filter.taskID}`);
|
||||
} else {
|
||||
const job = await serverProxy.jobs.getJob(filter.jobID);
|
||||
if (typeof (job.task_id) !== 'undefined') {
|
||||
tasks = await serverProxy.tasks.getTasks(`id=${job.task_id}`);
|
||||
}
|
||||
}
|
||||
|
||||
// If task was found by its id, then create task instance and get Job instance from it
|
||||
if (tasks !== null && tasks.length) {
|
||||
const task = new Task(tasks[0]);
|
||||
return filter.jobID ? task.jobs.filter(job => job.id === filter.jobID) : task.jobs;
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
cvat.tasks.get.implementation = async (filter) => {
|
||||
checkFilter(filter, {
|
||||
page: isInteger,
|
||||
name: isString,
|
||||
id: isInteger,
|
||||
owner: isString,
|
||||
assignee: isString,
|
||||
search: isString,
|
||||
status: isEnum.bind(TaskStatus),
|
||||
mode: isEnum.bind(TaskMode),
|
||||
});
|
||||
|
||||
if ('search' in filter && Object.keys(filter).length > 1) {
|
||||
if (!('page' in filter && Object.keys(filter).length === 2)) {
|
||||
throw new ArgumentError(
|
||||
'Do not use the filter field "search" with others',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ('id' in filter && Object.keys(filter).length > 1) {
|
||||
if (!('page' in filter && Object.keys(filter).length === 2)) {
|
||||
throw new ArgumentError(
|
||||
'Do not use the filter field "id" with others',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
for (const field of ['name', 'owner', 'assignee', 'search', 'status', 'mode', 'id', 'page']) {
|
||||
if (Object.prototype.hasOwnProperty.call(filter, field)) {
|
||||
searchParams.set(field, filter[field]);
|
||||
}
|
||||
}
|
||||
|
||||
const tasksData = await serverProxy.tasks.getTasks(searchParams.toString());
|
||||
const tasks = tasksData.map(task => new Task(task));
|
||||
tasks.count = tasksData.count;
|
||||
|
||||
return tasks;
|
||||
};
|
||||
|
||||
return cvat;
|
||||
}
|
||||
|
||||
module.exports = implementAPI;
|
||||
})();
|
||||
@ -0,0 +1,520 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:false
|
||||
*/
|
||||
|
||||
/**
|
||||
* External API which should be used by for development
|
||||
* @module API
|
||||
*/
|
||||
|
||||
function build() {
|
||||
const PluginRegistry = require('./plugins');
|
||||
const User = require('./user');
|
||||
const ObjectState = require('./object-state');
|
||||
const Statistics = require('./statistics');
|
||||
const { Job, Task } = require('./session');
|
||||
const { Attribute, Label } = require('./labels');
|
||||
|
||||
const {
|
||||
ShareFileType,
|
||||
TaskStatus,
|
||||
TaskMode,
|
||||
AttributeType,
|
||||
ObjectType,
|
||||
ObjectShape,
|
||||
VisibleState,
|
||||
LogType,
|
||||
} = require('./enums');
|
||||
|
||||
const {
|
||||
Exception,
|
||||
ArgumentError,
|
||||
DataError,
|
||||
ScriptingError,
|
||||
PluginError,
|
||||
ServerError,
|
||||
} = require('./exceptions');
|
||||
|
||||
const pjson = require('../package.json');
|
||||
const config = require('./config');
|
||||
|
||||
/**
|
||||
* API entrypoint
|
||||
* @namespace cvat
|
||||
* @memberof module:API
|
||||
*/
|
||||
const cvat = {
|
||||
/**
|
||||
* Namespace is used for an interaction with a server
|
||||
* @namespace server
|
||||
* @package
|
||||
* @memberof module:API.cvat
|
||||
*/
|
||||
server: {
|
||||
/**
|
||||
* @typedef {Object} ServerInfo
|
||||
* @property {string} name A name of the tool
|
||||
* @property {string} description A description of the tool
|
||||
* @property {string} version A version of the tool
|
||||
* @global
|
||||
*/
|
||||
|
||||
/**
|
||||
* Method returns some information about the annotation tool
|
||||
* @method about
|
||||
* @async
|
||||
* @memberof module:API.cvat.server
|
||||
* @return {ServerInfo}
|
||||
* @throws {module:API.cvat.exceptions.ServerError}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
*/
|
||||
async about() {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper(cvat.server.about);
|
||||
return result;
|
||||
},
|
||||
/**
|
||||
* @typedef {Object} FileInfo
|
||||
* @property {string} name A name of a file
|
||||
* @property {module:API.cvat.enums.ShareFileType} type
|
||||
* A type of a file
|
||||
* @global
|
||||
*/
|
||||
|
||||
/**
|
||||
* Method returns a list of files in a specified directory on a share
|
||||
* @method share
|
||||
* @async
|
||||
* @memberof module:API.cvat.server
|
||||
* @param {string} [directory=/] - Share directory path
|
||||
* @returns {FileInfo[]}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
* @throws {module:API.cvat.exceptions.ServerError}
|
||||
*/
|
||||
async share(directory = '/') {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper(cvat.server.share, directory);
|
||||
return result;
|
||||
},
|
||||
/**
|
||||
* Method returns available annotation formats
|
||||
* @method formats
|
||||
* @async
|
||||
* @memberof module:API.cvat.server
|
||||
* @returns {module:API.cvat.classes.AnnotationFormat[]}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
* @throws {module:API.cvat.exceptions.ServerError}
|
||||
*/
|
||||
async formats() {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper(cvat.server.formats);
|
||||
return result;
|
||||
},
|
||||
/**
|
||||
* Method allows to register on a server
|
||||
* @method register
|
||||
* @async
|
||||
* @memberof module:API.cvat.server
|
||||
* @param {string} username An username for the new account
|
||||
* @param {string} firstName A first name for the new account
|
||||
* @param {string} lastName A last name for the new account
|
||||
* @param {string} email A email address for the new account
|
||||
* @param {string} password1 A password for the new account
|
||||
* @param {string} password2 The confirmation password for the new account
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
* @throws {module:API.cvat.exceptions.ServerError}
|
||||
*/
|
||||
async register(username, firstName, lastName, email, password1, password2) {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper(cvat.server.register, username, firstName,
|
||||
lastName, email, password1, password2);
|
||||
return result;
|
||||
},
|
||||
/**
|
||||
* Method allows to login on a server
|
||||
* @method login
|
||||
* @async
|
||||
* @memberof module:API.cvat.server
|
||||
* @param {string} username An username of an account
|
||||
* @param {string} password A password of an account
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
* @throws {module:API.cvat.exceptions.ServerError}
|
||||
*/
|
||||
async login(username, password) {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper(cvat.server.login, username, password);
|
||||
return result;
|
||||
},
|
||||
/**
|
||||
* Method allows to logout from the server
|
||||
* @method logout
|
||||
* @async
|
||||
* @memberof module:API.cvat.server
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
* @throws {module:API.cvat.exceptions.ServerError}
|
||||
*/
|
||||
async logout() {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper(cvat.server.logout);
|
||||
return result;
|
||||
},
|
||||
/**
|
||||
* Method allows to know whether you are authorized on the server
|
||||
* @method authorized
|
||||
* @async
|
||||
* @memberof module:API.cvat.server
|
||||
* @returns {boolean}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
* @throws {module:API.cvat.exceptions.ServerError}
|
||||
*/
|
||||
async authorized() {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper(cvat.server.authorized);
|
||||
return result;
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Namespace is used for getting tasks
|
||||
* @namespace tasks
|
||||
* @memberof module:API.cvat
|
||||
*/
|
||||
tasks: {
|
||||
/**
|
||||
* @typedef {Object} TaskFilter
|
||||
* @property {string} name Check if name contains this value
|
||||
* @property {module:API.cvat.enums.TaskStatus} status
|
||||
* Check if status contains this value
|
||||
* @property {module:API.cvat.enums.TaskMode} mode
|
||||
* Check if mode contains this value
|
||||
* @property {integer} id Check if id equals this value
|
||||
* @property {integer} page Get specific page
|
||||
* (default REST API returns 20 tasks per request.
|
||||
* In order to get more, it is need to specify next page)
|
||||
* @property {string} owner Check if owner user contains this value
|
||||
* @property {string} assignee Check if assigneed contains this value
|
||||
* @property {string} search Combined search of contains among all fields
|
||||
* @global
|
||||
*/
|
||||
|
||||
/**
|
||||
* Method returns list of tasks corresponding to a filter
|
||||
* @method get
|
||||
* @async
|
||||
* @memberof module:API.cvat.tasks
|
||||
* @param {TaskFilter} [filter={}] task filter
|
||||
* @returns {module:API.cvat.classes.Task[]}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
* @throws {module:API.cvat.exceptions.ServerError}
|
||||
*/
|
||||
async get(filter = {}) {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper(cvat.tasks.get, filter);
|
||||
return result;
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Namespace is used for getting jobs
|
||||
* @namespace jobs
|
||||
* @memberof module:API.cvat
|
||||
*/
|
||||
jobs: {
|
||||
/**
|
||||
* @typedef {Object} JobFilter
|
||||
* Only one of fields is allowed simultaneously
|
||||
* @property {integer} taskID filter all jobs of specific task
|
||||
* @property {integer} jobID filter job with a specific id
|
||||
* @global
|
||||
*/
|
||||
|
||||
/**
|
||||
* Method returns list of jobs corresponding to a filter
|
||||
* @method get
|
||||
* @async
|
||||
* @memberof module:API.cvat.jobs
|
||||
* @param {JobFilter} filter job filter
|
||||
* @returns {module:API.cvat.classes.Job[]}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
* @throws {module:API.cvat.exceptions.ServerError}
|
||||
*/
|
||||
async get(filter = {}) {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper(cvat.jobs.get, filter);
|
||||
return result;
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Namespace is used for getting users
|
||||
* @namespace users
|
||||
* @memberof module:API.cvat
|
||||
*/
|
||||
users: {
|
||||
/**
|
||||
* @typedef {Object} UserFilter
|
||||
* @property {boolean} self get only self
|
||||
* @global
|
||||
*/
|
||||
|
||||
/**
|
||||
* Method returns list of users corresponding to a filter
|
||||
* @method get
|
||||
* @async
|
||||
* @memberof module:API.cvat.users
|
||||
* @param {UserFilter} [filter={}] user filter
|
||||
* @returns {module:API.cvat.classes.User[]}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
* @throws {module:API.cvat.exceptions.ServerError}
|
||||
*/
|
||||
async get(filter = {}) {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper(cvat.users.get, filter);
|
||||
return result;
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Namespace is used for plugin management
|
||||
* @namespace plugins
|
||||
* @memberof module:API.cvat
|
||||
*/
|
||||
plugins: {
|
||||
/**
|
||||
* @typedef {Object} Plugin
|
||||
* A plugin is a Javascript object. It must have properties are listed below. <br>
|
||||
* It also mustn't have property 'functions' which is used internally. <br>
|
||||
* You can expand any API method including class methods. <br>
|
||||
* In order to expand class method just use a class name
|
||||
* in a cvat space (example is listed below).
|
||||
*
|
||||
* @property {string} name A name of a plugin
|
||||
* @property {string} description A description of a plugin
|
||||
* Example plugin implementation listed below:
|
||||
* @example
|
||||
* plugin = {
|
||||
* name: 'Example Plugin',
|
||||
* description: 'This example plugin demonstrates how plugin system in CVAT works',
|
||||
* cvat: {
|
||||
* server: {
|
||||
* about: {
|
||||
* // Plugin adds some actions after executing the cvat.server.about()
|
||||
* // For example it adds a field with installed plugins to a result
|
||||
* // An argument "self" is a plugin itself
|
||||
* // An argument "result" is a return value of cvat.server.about()
|
||||
* // All next arguments are arguments of a wrapped function
|
||||
* // (in this case the wrapped function doesn't have any arguments)
|
||||
* async leave(self, result) {
|
||||
* result.plugins = await self.internal.getPlugins();
|
||||
* // Note that a method leave must return "result" (changed or not)
|
||||
* // Otherwise API won't work as expected
|
||||
* return result;
|
||||
* },
|
||||
* },
|
||||
* },
|
||||
* // In this example plugin also wraps a class method
|
||||
* classes: {
|
||||
* Job: {
|
||||
* prototype: {
|
||||
* annotations: {
|
||||
* put: {
|
||||
* // The first argument "self" is a plugin, like in a case above
|
||||
* // The second argument is an argument of the
|
||||
* // Job.annotations.put()
|
||||
* // It contains an array of objects to put
|
||||
* // In this sample we round objects coordinates and save them
|
||||
* enter(self, objects) {
|
||||
* for (const obj of objects) {
|
||||
* if (obj.type != 'tag') {
|
||||
* const points = obj.position.map((point) => {
|
||||
* const roundPoint = {
|
||||
* x: Math.round(point.x),
|
||||
* y: Math.round(point.y),
|
||||
* };
|
||||
* return roundPoint;
|
||||
* });
|
||||
* }
|
||||
* }
|
||||
* },
|
||||
* },
|
||||
* },
|
||||
* },
|
||||
* },
|
||||
* },
|
||||
* },
|
||||
* // In general you can add any others members to your plugin
|
||||
* // Members below are only examples
|
||||
* internal: {
|
||||
* async getPlugins() {
|
||||
* // Collect information about installed plugins
|
||||
* const plugins = await cvat.plugins.list();
|
||||
* return plugins.map((el) => {
|
||||
* return {
|
||||
* name: el.name,
|
||||
* description: el.description,
|
||||
* };
|
||||
* });
|
||||
* },
|
||||
* },
|
||||
* };
|
||||
* @global
|
||||
*/
|
||||
|
||||
/**
|
||||
* Method returns list of installed plugins
|
||||
* @method list
|
||||
* @async
|
||||
* @memberof module:API.cvat.plugins
|
||||
* @returns {Plugin[]}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
*/
|
||||
async list() {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper(cvat.plugins.list);
|
||||
return result;
|
||||
},
|
||||
/**
|
||||
* Install plugin to CVAT
|
||||
* @method register
|
||||
* @async
|
||||
* @memberof module:API.cvat.plugins
|
||||
* @param {Plugin} [plugin] plugin for registration
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
*/
|
||||
async register(plugin) {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper(cvat.plugins.register, plugin);
|
||||
return result;
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Namespace contains some changeable configurations
|
||||
* @namespace config
|
||||
* @memberof module:API.cvat
|
||||
*/
|
||||
config: {
|
||||
/**
|
||||
* @memberof module:API.cvat.config
|
||||
* @property {string} backendAPI host with a backend api
|
||||
* @memberof module:API.cvat.config
|
||||
* @property {string} proxy Axios proxy settings.
|
||||
* For more details please read <a href="https://github.com/axios/axios"> here </a>
|
||||
* @memberof module:API.cvat.config
|
||||
* @property {integer} taskID this value is displayed in a logs if available
|
||||
* @memberof module:API.cvat.config
|
||||
* @property {integer} jobID this value is displayed in a logs if available
|
||||
* @memberof module:API.cvat.config
|
||||
* @property {integer} clientID read only auto-generated
|
||||
* value which is displayed in a logs
|
||||
* @memberof module:API.cvat.config
|
||||
*/
|
||||
get backendAPI() {
|
||||
return config.backendAPI;
|
||||
},
|
||||
set backendAPI(value) {
|
||||
config.backendAPI = value;
|
||||
},
|
||||
get proxy() {
|
||||
return config.proxy;
|
||||
},
|
||||
set proxy(value) {
|
||||
config.proxy = value;
|
||||
},
|
||||
get taskID() {
|
||||
return config.taskID;
|
||||
},
|
||||
set taskID(value) {
|
||||
config.taskID = value;
|
||||
},
|
||||
get jobID() {
|
||||
return config.jobID;
|
||||
},
|
||||
set jobID(value) {
|
||||
config.jobID = value;
|
||||
},
|
||||
get clientID() {
|
||||
return config.clientID;
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Namespace contains some library information e.g. api version
|
||||
* @namespace client
|
||||
* @memberof module:API.cvat
|
||||
*/
|
||||
client: {
|
||||
/**
|
||||
* @property {string} version Client version.
|
||||
* Format: <b>{major}.{minor}.{patch}</b>
|
||||
* <li style="margin-left: 10px;"> A major number is changed after an API becomes
|
||||
* incompatible with a previous version
|
||||
* <li style="margin-left: 10px;"> A minor number is changed after an API expands
|
||||
* <li style="margin-left: 10px;"> A patch number is changed after an each build
|
||||
* @memberof module:API.cvat.client
|
||||
* @readonly
|
||||
*/
|
||||
version: `${pjson.version}`,
|
||||
},
|
||||
/**
|
||||
* Namespace is used for access to enums
|
||||
* @namespace enums
|
||||
* @memberof module:API.cvat
|
||||
*/
|
||||
enums: {
|
||||
ShareFileType,
|
||||
TaskStatus,
|
||||
TaskMode,
|
||||
AttributeType,
|
||||
ObjectType,
|
||||
ObjectShape,
|
||||
VisibleState,
|
||||
LogType,
|
||||
},
|
||||
/**
|
||||
* Namespace is used for access to exceptions
|
||||
* @namespace exceptions
|
||||
* @memberof module:API.cvat
|
||||
*/
|
||||
exceptions: {
|
||||
Exception,
|
||||
ArgumentError,
|
||||
DataError,
|
||||
ScriptingError,
|
||||
PluginError,
|
||||
ServerError,
|
||||
},
|
||||
/**
|
||||
* Namespace is used for access to classes
|
||||
* @namespace classes
|
||||
* @memberof module:API.cvat
|
||||
*/
|
||||
classes: {
|
||||
Task,
|
||||
User,
|
||||
Job,
|
||||
Attribute,
|
||||
Label,
|
||||
Statistics,
|
||||
ObjectState,
|
||||
},
|
||||
};
|
||||
|
||||
cvat.server = Object.freeze(cvat.server);
|
||||
cvat.tasks = Object.freeze(cvat.tasks);
|
||||
cvat.jobs = Object.freeze(cvat.jobs);
|
||||
cvat.users = Object.freeze(cvat.users);
|
||||
cvat.plugins = Object.freeze(cvat.plugins);
|
||||
cvat.client = Object.freeze(cvat.client);
|
||||
cvat.enums = Object.freeze(cvat.enums);
|
||||
|
||||
const implementAPI = require('./api-implementation');
|
||||
|
||||
Math.clamp = function (value, min, max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
};
|
||||
|
||||
const implemented = Object.freeze(implementAPI(cvat));
|
||||
return implemented;
|
||||
}
|
||||
|
||||
module.exports = build();
|
||||
@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:false
|
||||
*/
|
||||
|
||||
(() => {
|
||||
const { ArgumentError } = require('./exceptions');
|
||||
|
||||
function isBoolean(value) {
|
||||
return typeof (value) === 'boolean';
|
||||
}
|
||||
|
||||
function isInteger(value) {
|
||||
return typeof (value) === 'number' && Number.isInteger(value);
|
||||
}
|
||||
|
||||
// Called with specific Enum context
|
||||
function isEnum(value) {
|
||||
for (const key in this) {
|
||||
if (Object.prototype.hasOwnProperty.call(this, key)) {
|
||||
if (this[key] === value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function isString(value) {
|
||||
return typeof (value) === 'string';
|
||||
}
|
||||
|
||||
function checkFilter(filter, fields) {
|
||||
for (const prop in filter) {
|
||||
if (Object.prototype.hasOwnProperty.call(filter, prop)) {
|
||||
if (!(prop in fields)) {
|
||||
throw new ArgumentError(
|
||||
`Unsupported filter property has been recieved: "${prop}"`,
|
||||
);
|
||||
} else if (!fields[prop](filter[prop])) {
|
||||
throw new ArgumentError(
|
||||
`Received filter property "${prop}" is not satisfied for checker`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkObjectType(name, value, type, instance) {
|
||||
if (type) {
|
||||
if (typeof (value) !== type) {
|
||||
// specific case for integers which aren't native type in JS
|
||||
if (type === 'integer' && Number.isInteger(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ArgumentError(
|
||||
`"${name}" is expected to be "${type}", but "${typeof (value)}" has been got.`,
|
||||
);
|
||||
}
|
||||
} else if (instance) {
|
||||
if (!(value instanceof instance)) {
|
||||
if (value !== undefined) {
|
||||
throw new ArgumentError(
|
||||
`"${name}" is expected to be ${instance.name}, but `
|
||||
+ `"${value.constructor.name}" has been got`,
|
||||
);
|
||||
}
|
||||
|
||||
throw new ArgumentError(
|
||||
`"${name}" is expected to be ${instance.name}, but "undefined" has been got.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isBoolean,
|
||||
isInteger,
|
||||
isEnum,
|
||||
isString,
|
||||
checkFilter,
|
||||
checkObjectType,
|
||||
};
|
||||
})();
|
||||
@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
backendAPI: 'http://localhost:7000/api/v1',
|
||||
proxy: false,
|
||||
taskID: undefined,
|
||||
jobID: undefined,
|
||||
clientID: +Date.now().toString().substr(-6),
|
||||
};
|
||||
@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
(() => {
|
||||
/**
|
||||
* Share files types
|
||||
* @enum {string}
|
||||
* @name ShareFileType
|
||||
* @memberof module:API.cvat.enums
|
||||
* @property {string} DIR 'DIR'
|
||||
* @property {string} REG 'REG'
|
||||
* @readonly
|
||||
*/
|
||||
const ShareFileType = Object.freeze({
|
||||
DIR: 'DIR',
|
||||
REG: 'REG',
|
||||
});
|
||||
|
||||
/**
|
||||
* Task statuses
|
||||
* @enum {string}
|
||||
* @name TaskStatus
|
||||
* @memberof module:API.cvat.enums
|
||||
* @property {string} ANNOTATION 'annotation'
|
||||
* @property {string} VALIDATION 'validation'
|
||||
* @property {string} COMPLETED 'completed'
|
||||
* @readonly
|
||||
*/
|
||||
const TaskStatus = Object.freeze({
|
||||
ANNOTATION: 'annotation',
|
||||
VALIDATION: 'validation',
|
||||
COMPLETED: 'completed',
|
||||
});
|
||||
|
||||
/**
|
||||
* Task modes
|
||||
* @enum {string}
|
||||
* @name TaskMode
|
||||
* @memberof module:API.cvat.enums
|
||||
* @property {string} ANNOTATION 'annotation'
|
||||
* @property {string} INTERPOLATION 'interpolation'
|
||||
* @readonly
|
||||
*/
|
||||
const TaskMode = Object.freeze({
|
||||
ANNOTATION: 'annotation',
|
||||
INTERPOLATION: 'interpolation',
|
||||
});
|
||||
|
||||
/**
|
||||
* Attribute types
|
||||
* @enum {string}
|
||||
* @name AttributeType
|
||||
* @memberof module:API.cvat.enums
|
||||
* @property {string} CHECKBOX 'checkbox'
|
||||
* @property {string} SELECT 'select'
|
||||
* @property {string} RADIO 'radio'
|
||||
* @property {string} NUMBER 'number'
|
||||
* @property {string} TEXT 'text'
|
||||
* @readonly
|
||||
*/
|
||||
const AttributeType = Object.freeze({
|
||||
CHECKBOX: 'checkbox',
|
||||
RADIO: 'radio',
|
||||
SELECT: 'select',
|
||||
NUMBER: 'number',
|
||||
TEXT: 'text',
|
||||
});
|
||||
|
||||
/**
|
||||
* Object types
|
||||
* @enum {string}
|
||||
* @name ObjectType
|
||||
* @memberof module:API.cvat.enums
|
||||
* @property {string} TAG 'tag'
|
||||
* @property {string} SHAPE 'shape'
|
||||
* @property {string} TRACK 'track'
|
||||
* @readonly
|
||||
*/
|
||||
const ObjectType = Object.freeze({
|
||||
TAG: 'tag',
|
||||
SHAPE: 'shape',
|
||||
TRACK: 'track',
|
||||
});
|
||||
|
||||
/**
|
||||
* Object shapes
|
||||
* @enum {string}
|
||||
* @name ObjectShape
|
||||
* @memberof module:API.cvat.enums
|
||||
* @property {string} RECTANGLE 'rectangle'
|
||||
* @property {string} POLYGON 'polygon'
|
||||
* @property {string} POLYLINE 'polyline'
|
||||
* @property {string} POINTS 'points'
|
||||
* @readonly
|
||||
*/
|
||||
const ObjectShape = Object.freeze({
|
||||
RECTANGLE: 'rectangle',
|
||||
POLYGON: 'polygon',
|
||||
POLYLINE: 'polyline',
|
||||
POINTS: 'points',
|
||||
});
|
||||
|
||||
/**
|
||||
* Object visibility states
|
||||
* @enum {string}
|
||||
* @name ObjectShape
|
||||
* @memberof module:API.cvat.enums
|
||||
* @property {string} ALL 'all'
|
||||
* @property {string} SHAPE 'shape'
|
||||
* @property {string} NONE 'none'
|
||||
* @readonly
|
||||
*/
|
||||
const VisibleState = Object.freeze({
|
||||
ALL: 'all',
|
||||
SHAPE: 'shape',
|
||||
NONE: 'none',
|
||||
});
|
||||
|
||||
/**
|
||||
* Event types
|
||||
* @enum {number}
|
||||
* @name LogType
|
||||
* @memberof module:API.cvat.enums
|
||||
* @property {number} pasteObject 0
|
||||
* @property {number} changeAttribute 1
|
||||
* @property {number} dragObject 2
|
||||
* @property {number} deleteObject 3
|
||||
* @property {number} pressShortcut 4
|
||||
* @property {number} resizeObject 5
|
||||
* @property {number} sendLogs 6
|
||||
* @property {number} saveJob 7
|
||||
* @property {number} jumpFrame 8
|
||||
* @property {number} drawObject 9
|
||||
* @property {number} changeLabel 10
|
||||
* @property {number} sendTaskInfo 11
|
||||
* @property {number} loadJob 12
|
||||
* @property {number} moveImage 13
|
||||
* @property {number} zoomImage 14
|
||||
* @property {number} lockObject 15
|
||||
* @property {number} mergeObjects 16
|
||||
* @property {number} copyObject 17
|
||||
* @property {number} propagateObject 18
|
||||
* @property {number} undoAction 19
|
||||
* @property {number} redoAction 20
|
||||
* @property {number} sendUserActivity 21
|
||||
* @property {number} sendException 22
|
||||
* @property {number} changeFrame 23
|
||||
* @property {number} debugInfo 24
|
||||
* @property {number} fitImage 25
|
||||
* @property {number} rotateImage 26
|
||||
* @readonly
|
||||
*/
|
||||
const LogType = {
|
||||
pasteObject: 0,
|
||||
changeAttribute: 1,
|
||||
dragObject: 2,
|
||||
deleteObject: 3,
|
||||
pressShortcut: 4,
|
||||
resizeObject: 5,
|
||||
sendLogs: 6,
|
||||
saveJob: 7,
|
||||
jumpFrame: 8,
|
||||
drawObject: 9,
|
||||
changeLabel: 10,
|
||||
sendTaskInfo: 11,
|
||||
loadJob: 12,
|
||||
moveImage: 13,
|
||||
zoomImage: 14,
|
||||
lockObject: 15,
|
||||
mergeObjects: 16,
|
||||
copyObject: 17,
|
||||
propagateObject: 18,
|
||||
undoAction: 19,
|
||||
redoAction: 20,
|
||||
sendUserActivity: 21,
|
||||
sendException: 22,
|
||||
changeFrame: 23,
|
||||
debugInfo: 24,
|
||||
fitImage: 25,
|
||||
rotateImage: 26,
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
ShareFileType,
|
||||
TaskStatus,
|
||||
TaskMode,
|
||||
AttributeType,
|
||||
ObjectType,
|
||||
ObjectShape,
|
||||
VisibleState,
|
||||
LogType,
|
||||
};
|
||||
})();
|
||||
@ -0,0 +1,272 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:false
|
||||
*/
|
||||
|
||||
(() => {
|
||||
const Platform = require('platform');
|
||||
const ErrorStackParser = require('error-stack-parser');
|
||||
const config = require('./config');
|
||||
|
||||
/**
|
||||
* Base exception class
|
||||
* @memberof module:API.cvat.exceptions
|
||||
* @extends Error
|
||||
* @ignore
|
||||
*/
|
||||
class Exception extends Error {
|
||||
/**
|
||||
* @param {string} message - Exception message
|
||||
*/
|
||||
constructor(message) {
|
||||
super(message);
|
||||
|
||||
const time = new Date().toISOString();
|
||||
const system = Platform.os.toString();
|
||||
const client = `${Platform.name} ${Platform.version}`;
|
||||
const info = ErrorStackParser.parse(this)[0];
|
||||
const filename = `${info.fileName}`;
|
||||
const line = info.lineNumber;
|
||||
const column = info.columnNumber;
|
||||
const {
|
||||
jobID,
|
||||
taskID,
|
||||
clientID,
|
||||
} = config;
|
||||
|
||||
const projID = undefined; // wasn't implemented
|
||||
|
||||
Object.defineProperties(this, Object.freeze({
|
||||
system: {
|
||||
/**
|
||||
* @name system
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.exceptions.Exception
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => system,
|
||||
},
|
||||
client: {
|
||||
/**
|
||||
* @name client
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.exceptions.Exception
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => client,
|
||||
},
|
||||
time: {
|
||||
/**
|
||||
* @name time
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.exceptions.Exception
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => time,
|
||||
},
|
||||
jobID: {
|
||||
/**
|
||||
* @name jobID
|
||||
* @type {integer}
|
||||
* @memberof module:API.cvat.exceptions.Exception
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => jobID,
|
||||
},
|
||||
taskID: {
|
||||
/**
|
||||
* @name taskID
|
||||
* @type {integer}
|
||||
* @memberof module:API.cvat.exceptions.Exception
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => taskID,
|
||||
},
|
||||
projID: {
|
||||
/**
|
||||
* @name projID
|
||||
* @type {integer}
|
||||
* @memberof module:API.cvat.exceptions.Exception
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => projID,
|
||||
},
|
||||
clientID: {
|
||||
/**
|
||||
* @name clientID
|
||||
* @type {integer}
|
||||
* @memberof module:API.cvat.exceptions.Exception
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => clientID,
|
||||
},
|
||||
filename: {
|
||||
/**
|
||||
* @name filename
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.exceptions.Exception
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => filename,
|
||||
},
|
||||
line: {
|
||||
/**
|
||||
* @name line
|
||||
* @type {integer}
|
||||
* @memberof module:API.cvat.exceptions.Exception
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => line,
|
||||
},
|
||||
column: {
|
||||
/**
|
||||
* @name column
|
||||
* @type {integer}
|
||||
* @memberof module:API.cvat.exceptions.Exception
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => column,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an exception on a server
|
||||
* @name save
|
||||
* @method
|
||||
* @memberof Exception
|
||||
* @instance
|
||||
* @async
|
||||
*/
|
||||
async save() {
|
||||
const exceptionObject = {
|
||||
system: this.system,
|
||||
client: this.client,
|
||||
time: this.time,
|
||||
job_id: this.jobID,
|
||||
task_id: this.taskID,
|
||||
proj_id: this.projID,
|
||||
client_id: this.clientID,
|
||||
message: this.message,
|
||||
filename: this.filename,
|
||||
line: this.line,
|
||||
column: this.column,
|
||||
stack: this.stack,
|
||||
};
|
||||
|
||||
try {
|
||||
const serverProxy = require('./server-proxy');
|
||||
await serverProxy.server.exception(exceptionObject);
|
||||
} catch (exception) {
|
||||
// add event
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exceptions are referred with arguments data
|
||||
* @memberof module:API.cvat.exceptions
|
||||
* @extends module:API.cvat.exceptions.Exception
|
||||
*/
|
||||
class ArgumentError extends Exception {
|
||||
/**
|
||||
* @param {string} message - Exception message
|
||||
*/
|
||||
constructor(message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unexpected problems with data which are not connected with a user input
|
||||
* @memberof module:API.cvat.exceptions
|
||||
* @extends module:API.cvat.exceptions.Exception
|
||||
*/
|
||||
class DataError extends Exception {
|
||||
/**
|
||||
* @param {string} message - Exception message
|
||||
*/
|
||||
constructor(message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unexpected situations in code
|
||||
* @memberof module:API.cvat.exceptions
|
||||
* @extends module:API.cvat.exceptions.Exception
|
||||
*/
|
||||
class ScriptingError extends Exception {
|
||||
/**
|
||||
* @param {string} message - Exception message
|
||||
*/
|
||||
constructor(message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin-referred exceptions
|
||||
* @memberof module:API.cvat.exceptions
|
||||
* @extends module:API.cvat.exceptions.Exception
|
||||
*/
|
||||
class PluginError extends Exception {
|
||||
/**
|
||||
* @param {string} message - Exception message
|
||||
*/
|
||||
constructor(message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exceptions in interaction with a server
|
||||
* @memberof module:API.cvat.exceptions
|
||||
* @extends module:API.cvat.exceptions.Exception
|
||||
*/
|
||||
class ServerError extends Exception {
|
||||
/**
|
||||
* @param {string} message - Exception message
|
||||
* @param {(string|integer)} code - Response code
|
||||
*/
|
||||
constructor(message, code) {
|
||||
super(message);
|
||||
|
||||
Object.defineProperties(this, Object.freeze({
|
||||
/**
|
||||
* @name code
|
||||
* @type {(string|integer)}
|
||||
* @memberof module:API.cvat.exceptions.ServerError
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
code: {
|
||||
get: () => code,
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Exception,
|
||||
ArgumentError,
|
||||
DataError,
|
||||
ScriptingError,
|
||||
PluginError,
|
||||
ServerError,
|
||||
};
|
||||
})();
|
||||
@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:false
|
||||
global:false
|
||||
*/
|
||||
|
||||
(() => {
|
||||
const PluginRegistry = require('./plugins');
|
||||
const serverProxy = require('./server-proxy');
|
||||
const { ArgumentError } = require('./exceptions');
|
||||
const { isBrowser, isNode } = require('browser-or-node');
|
||||
|
||||
// This is the frames storage
|
||||
const frameDataCache = {};
|
||||
const frameCache = {};
|
||||
|
||||
/**
|
||||
* Class provides meta information about specific frame and frame itself
|
||||
* @memberof module:API.cvat.classes
|
||||
* @hideconstructor
|
||||
*/
|
||||
class FrameData {
|
||||
constructor(width, height, tid, number) {
|
||||
Object.defineProperties(this, Object.freeze({
|
||||
/**
|
||||
* @name width
|
||||
* @type {integer}
|
||||
* @memberof module:API.cvat.classes.FrameData
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
width: {
|
||||
value: width,
|
||||
writable: false,
|
||||
},
|
||||
/**
|
||||
* @name height
|
||||
* @type {integer}
|
||||
* @memberof module:API.cvat.classes.FrameData
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
height: {
|
||||
value: height,
|
||||
writable: false,
|
||||
},
|
||||
tid: {
|
||||
value: tid,
|
||||
writable: false,
|
||||
},
|
||||
number: {
|
||||
value: number,
|
||||
writable: false,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method returns URL encoded image which can be placed in the img tag
|
||||
* @method data
|
||||
* @returns {string}
|
||||
* @memberof module:API.cvat.classes.FrameData
|
||||
* @instance
|
||||
* @async
|
||||
* @param {function} [onServerRequest = () => {}]
|
||||
* callback which will be called if data absences local
|
||||
* @throws {module:API.cvat.exception.ServerError}
|
||||
* @throws {module:API.cvat.exception.PluginError}
|
||||
*/
|
||||
async data(onServerRequest = () => {}) {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper.call(this, FrameData.prototype.data, onServerRequest);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
FrameData.prototype.data.implementation = async function (onServerRequest) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
if (this.number in frameCache[this.tid]) {
|
||||
resolve(frameCache[this.tid][this.number]);
|
||||
} else {
|
||||
onServerRequest();
|
||||
const frame = await serverProxy.frames.getData(this.tid, this.number);
|
||||
|
||||
if (isNode) {
|
||||
frameCache[this.tid][this.number] = global.Buffer.from(frame, 'binary').toString('base64');
|
||||
resolve(frameCache[this.tid][this.number]);
|
||||
} else if (isBrowser) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
frameCache[this.tid][this.number] = reader.result;
|
||||
resolve(frameCache[this.tid][this.number]);
|
||||
};
|
||||
reader.readAsDataURL(frame);
|
||||
}
|
||||
}
|
||||
} catch (exception) {
|
||||
reject(exception);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
async function getFrame(taskID, mode, frame) {
|
||||
if (!(taskID in frameDataCache)) {
|
||||
frameDataCache[taskID] = {};
|
||||
frameDataCache[taskID].meta = await serverProxy.frames.getMeta(taskID);
|
||||
|
||||
frameCache[taskID] = {};
|
||||
}
|
||||
|
||||
if (!(frame in frameDataCache[taskID])) {
|
||||
let size = null;
|
||||
if (mode === 'interpolation') {
|
||||
[size] = frameDataCache[taskID].meta;
|
||||
} else if (mode === 'annotation') {
|
||||
if (frame >= frameDataCache[taskID].meta.length) {
|
||||
throw new ArgumentError(
|
||||
`Meta information about frame ${frame} can't be received from the server`,
|
||||
);
|
||||
} else {
|
||||
size = frameDataCache[taskID].meta[frame];
|
||||
}
|
||||
} else {
|
||||
throw new ArgumentError(
|
||||
`Invalid mode is specified ${mode}`,
|
||||
);
|
||||
}
|
||||
|
||||
frameDataCache[taskID][frame] = new FrameData(size.width, size.height, taskID, frame);
|
||||
}
|
||||
|
||||
return frameDataCache[taskID][frame];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
FrameData,
|
||||
getFrame,
|
||||
};
|
||||
})();
|
||||
@ -0,0 +1,210 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:false
|
||||
*/
|
||||
|
||||
(() => {
|
||||
const { AttributeType } = require('./enums');
|
||||
const { ArgumentError } = require('./exceptions');
|
||||
|
||||
/**
|
||||
* Class representing an attribute
|
||||
* @memberof module:API.cvat.classes
|
||||
* @hideconstructor
|
||||
*/
|
||||
class Attribute {
|
||||
constructor(initialData) {
|
||||
const data = {
|
||||
id: undefined,
|
||||
default_value: undefined,
|
||||
input_type: undefined,
|
||||
mutable: undefined,
|
||||
name: undefined,
|
||||
values: undefined,
|
||||
};
|
||||
|
||||
for (const key in data) {
|
||||
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
||||
if (Object.prototype.hasOwnProperty.call(initialData, key)) {
|
||||
if (Array.isArray(initialData[key])) {
|
||||
data[key] = [...initialData[key]];
|
||||
} else {
|
||||
data[key] = initialData[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Object.values(AttributeType).includes(data.input_type)) {
|
||||
throw new ArgumentError(
|
||||
`Got invalid attribute type ${data.input_type}`,
|
||||
);
|
||||
}
|
||||
|
||||
Object.defineProperties(this, Object.freeze({
|
||||
/**
|
||||
* @name id
|
||||
* @type {integer}
|
||||
* @memberof module:API.cvat.classes.Attribute
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
id: {
|
||||
get: () => data.id,
|
||||
},
|
||||
/**
|
||||
* @name defaultValue
|
||||
* @type {(string|integer|boolean)}
|
||||
* @memberof module:API.cvat.classes.Attribute
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
defaultValue: {
|
||||
get: () => data.default_value,
|
||||
},
|
||||
/**
|
||||
* @name inputType
|
||||
* @type {module:API.cvat.enums.AttributeType}
|
||||
* @memberof module:API.cvat.classes.Attribute
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
inputType: {
|
||||
get: () => data.input_type,
|
||||
},
|
||||
/**
|
||||
* @name mutable
|
||||
* @type {boolean}
|
||||
* @memberof module:API.cvat.classes.Attribute
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
mutable: {
|
||||
get: () => data.mutable,
|
||||
},
|
||||
/**
|
||||
* @name name
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.Attribute
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
name: {
|
||||
get: () => data.name,
|
||||
},
|
||||
/**
|
||||
* @name values
|
||||
* @type {(string[]|integer[]|boolean[])}
|
||||
* @memberof module:API.cvat.classes.Attribute
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
values: {
|
||||
get: () => [...data.values],
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const object = {
|
||||
name: this.name,
|
||||
mutable: this.mutable,
|
||||
input_type: this.inputType,
|
||||
default_value: this.defaultValue,
|
||||
values: this.values,
|
||||
};
|
||||
|
||||
if (typeof (this.id) !== 'undefined') {
|
||||
object.id = this.id;
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class representing a label
|
||||
* @memberof module:API.cvat.classes
|
||||
* @hideconstructor
|
||||
*/
|
||||
class Label {
|
||||
constructor(initialData) {
|
||||
const data = {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
};
|
||||
|
||||
for (const key in data) {
|
||||
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
||||
if (Object.prototype.hasOwnProperty.call(initialData, key)) {
|
||||
data[key] = initialData[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data.attributes = [];
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(initialData, 'attributes')
|
||||
&& Array.isArray(initialData.attributes)) {
|
||||
for (const attrData of initialData.attributes) {
|
||||
data.attributes.push(new Attribute(attrData));
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperties(this, Object.freeze({
|
||||
/**
|
||||
* @name id
|
||||
* @type {integer}
|
||||
* @memberof module:API.cvat.classes.Label
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
id: {
|
||||
get: () => data.id,
|
||||
},
|
||||
/**
|
||||
* @name name
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.Label
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
name: {
|
||||
get: () => data.name,
|
||||
},
|
||||
/**
|
||||
* @name attributes
|
||||
* @type {module:API.cvat.classes.Attribute[]}
|
||||
* @memberof module:API.cvat.classes.Label
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
attributes: {
|
||||
get: () => [...data.attributes],
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const object = {
|
||||
name: this.name,
|
||||
attributes: [...this.attributes.map(el => el.toJSON())],
|
||||
};
|
||||
|
||||
if (typeof (this.id) !== 'undefined') {
|
||||
object.id = this.id;
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Attribute,
|
||||
Label,
|
||||
};
|
||||
})();
|
||||
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:false
|
||||
*/
|
||||
|
||||
(() => {
|
||||
const PluginRegistry = require('./plugins');
|
||||
|
||||
/**
|
||||
* Class describe scheme of a log object
|
||||
* @memberof module:API.cvat.classes
|
||||
* @hideconstructor
|
||||
*/
|
||||
class Log {
|
||||
constructor(logType, continuous, details) {
|
||||
this.type = logType;
|
||||
this.continuous = continuous;
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method closes a continue log
|
||||
* @method close
|
||||
* @memberof module:API.cvat.classes.Log
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
*/
|
||||
async close() {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper.call(this, Log.prototype.close);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Log;
|
||||
})();
|
||||
@ -0,0 +1,419 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:false
|
||||
*/
|
||||
|
||||
(() => {
|
||||
const PluginRegistry = require('./plugins');
|
||||
const { ArgumentError } = require('./exceptions');
|
||||
|
||||
/**
|
||||
* Class representing a state of an object on a specific frame
|
||||
* @memberof module:API.cvat.classes
|
||||
*/
|
||||
class ObjectState {
|
||||
/**
|
||||
* @param {Object} serialized - is an dictionary which contains
|
||||
* initial information about an ObjectState;
|
||||
* Necessary fields: objectType, shapeType
|
||||
* (don't have setters)
|
||||
* Necessary fields for objects which haven't been added to collection yet: frame
|
||||
* (doesn't have setters)
|
||||
* Optional fields: points, group, zOrder, outside, occluded,
|
||||
* attributes, lock, label, mode, color, keyframe, clientID, serverID
|
||||
* These fields can be set later via setters
|
||||
*/
|
||||
constructor(serialized) {
|
||||
const data = {
|
||||
label: null,
|
||||
attributes: {},
|
||||
|
||||
points: null,
|
||||
outside: null,
|
||||
occluded: null,
|
||||
keyframe: null,
|
||||
|
||||
group: null,
|
||||
zOrder: null,
|
||||
lock: null,
|
||||
color: null,
|
||||
visibility: null,
|
||||
|
||||
clientID: serialized.clientID,
|
||||
serverID: serialized.serverID,
|
||||
|
||||
frame: serialized.frame,
|
||||
objectType: serialized.objectType,
|
||||
shapeType: serialized.shapeType,
|
||||
updateFlags: {},
|
||||
};
|
||||
|
||||
// Shows whether any properties updated since last reset() or interpolation
|
||||
Object.defineProperty(data.updateFlags, 'reset', {
|
||||
value: function reset() {
|
||||
this.label = false;
|
||||
this.attributes = false;
|
||||
|
||||
this.points = false;
|
||||
this.outside = false;
|
||||
this.occluded = false;
|
||||
this.keyframe = false;
|
||||
|
||||
this.group = false;
|
||||
this.zOrder = false;
|
||||
this.lock = false;
|
||||
this.color = false;
|
||||
this.visibility = false;
|
||||
},
|
||||
writable: false,
|
||||
});
|
||||
|
||||
Object.defineProperties(this, Object.freeze({
|
||||
// Internal property. We don't need document it.
|
||||
updateFlags: {
|
||||
get: () => data.updateFlags,
|
||||
},
|
||||
frame: {
|
||||
/**
|
||||
* @name frame
|
||||
* @type {integer}
|
||||
* @memberof module:API.cvat.classes.ObjectState
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.frame,
|
||||
},
|
||||
objectType: {
|
||||
/**
|
||||
* @name objectType
|
||||
* @type {module:API.cvat.enums.ObjectType}
|
||||
* @memberof module:API.cvat.classes.ObjectState
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.objectType,
|
||||
},
|
||||
shapeType: {
|
||||
/**
|
||||
* @name shapeType
|
||||
* @type {module:API.cvat.enums.ObjectShape}
|
||||
* @memberof module:API.cvat.classes.ObjectState
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.shapeType,
|
||||
},
|
||||
clientID: {
|
||||
/**
|
||||
* @name clientID
|
||||
* @type {integer}
|
||||
* @memberof module:API.cvat.classes.ObjectState
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.clientID,
|
||||
},
|
||||
serverID: {
|
||||
/**
|
||||
* @name serverID
|
||||
* @type {integer}
|
||||
* @memberof module:API.cvat.classes.ObjectState
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.serverID,
|
||||
},
|
||||
label: {
|
||||
/**
|
||||
* @name shape
|
||||
* @type {module:API.cvat.classes.Label}
|
||||
* @memberof module:API.cvat.classes.ObjectState
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.label,
|
||||
set: (labelInstance) => {
|
||||
data.updateFlags.label = true;
|
||||
data.label = labelInstance;
|
||||
},
|
||||
},
|
||||
color: {
|
||||
/**
|
||||
* @name color
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.ObjectState
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.color,
|
||||
set: (color) => {
|
||||
data.updateFlags.color = true;
|
||||
data.color = color;
|
||||
},
|
||||
},
|
||||
visibility: {
|
||||
/**
|
||||
* @name visibility
|
||||
* @type {module:API.cvat.enums.VisibleState}
|
||||
* @memberof module:API.cvat.classes.ObjectState
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.visibility,
|
||||
set: (visibility) => {
|
||||
data.updateFlags.visibility = true;
|
||||
data.visibility = visibility;
|
||||
},
|
||||
},
|
||||
points: {
|
||||
/**
|
||||
* @name points
|
||||
* @type {number[]}
|
||||
* @memberof module:API.cvat.classes.ObjectState
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.points,
|
||||
set: (points) => {
|
||||
if (Array.isArray(points)) {
|
||||
data.updateFlags.points = true;
|
||||
data.points = [...points];
|
||||
} else {
|
||||
throw new ArgumentError(
|
||||
'Points are expected to be an array '
|
||||
+ `but got ${typeof (points) === 'object'
|
||||
? points.constructor.name : typeof (points)}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
group: {
|
||||
/**
|
||||
* @name group
|
||||
* @type {integer}
|
||||
* @memberof module:API.cvat.classes.ObjectState
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.group,
|
||||
set: (group) => {
|
||||
data.updateFlags.group = true;
|
||||
data.group = group;
|
||||
},
|
||||
},
|
||||
zOrder: {
|
||||
/**
|
||||
* @name zOrder
|
||||
* @type {integer}
|
||||
* @memberof module:API.cvat.classes.ObjectState
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.zOrder,
|
||||
set: (zOrder) => {
|
||||
data.updateFlags.zOrder = true;
|
||||
data.zOrder = zOrder;
|
||||
},
|
||||
},
|
||||
outside: {
|
||||
/**
|
||||
* @name outside
|
||||
* @type {boolean}
|
||||
* @memberof module:API.cvat.classes.ObjectState
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.outside,
|
||||
set: (outside) => {
|
||||
data.updateFlags.outside = true;
|
||||
data.outside = outside;
|
||||
},
|
||||
},
|
||||
keyframe: {
|
||||
/**
|
||||
* @name keyframe
|
||||
* @type {boolean}
|
||||
* @memberof module:API.cvat.classes.ObjectState
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.keyframe,
|
||||
set: (keyframe) => {
|
||||
data.updateFlags.keyframe = true;
|
||||
data.keyframe = keyframe;
|
||||
},
|
||||
},
|
||||
occluded: {
|
||||
/**
|
||||
* @name occluded
|
||||
* @type {boolean}
|
||||
* @memberof module:API.cvat.classes.ObjectState
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.occluded,
|
||||
set: (occluded) => {
|
||||
data.updateFlags.occluded = true;
|
||||
data.occluded = occluded;
|
||||
},
|
||||
},
|
||||
lock: {
|
||||
/**
|
||||
* @name lock
|
||||
* @type {boolean}
|
||||
* @memberof module:API.cvat.classes.ObjectState
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.lock,
|
||||
set: (lock) => {
|
||||
data.updateFlags.lock = true;
|
||||
data.lock = lock;
|
||||
},
|
||||
},
|
||||
attributes: {
|
||||
/**
|
||||
* Object is id:value pairs where "id" is an integer
|
||||
* attribute identifier and "value" is an attribute value
|
||||
* @name attributes
|
||||
* @type {Object}
|
||||
* @memberof module:API.cvat.classes.ObjectState
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.attributes,
|
||||
set: (attributes) => {
|
||||
if (typeof (attributes) !== 'object') {
|
||||
throw new ArgumentError(
|
||||
'Attributes are expected to be an object '
|
||||
+ `but got ${typeof (attributes) === 'object'
|
||||
? attributes.constructor.name : typeof (attributes)}`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const attrID of Object.keys(attributes)) {
|
||||
data.updateFlags.attributes = true;
|
||||
data.attributes[attrID] = attributes[attrID];
|
||||
}
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
this.label = serialized.label;
|
||||
this.group = serialized.group;
|
||||
this.zOrder = serialized.zOrder;
|
||||
this.outside = serialized.outside;
|
||||
this.keyframe = serialized.keyframe;
|
||||
this.occluded = serialized.occluded;
|
||||
this.color = serialized.color;
|
||||
this.lock = serialized.lock;
|
||||
this.visibility = serialized.visibility;
|
||||
|
||||
// It can be undefined in a constructor and it can be defined later
|
||||
if (typeof (serialized.points) !== 'undefined') {
|
||||
this.points = serialized.points;
|
||||
}
|
||||
if (typeof (serialized.attributes) !== 'undefined') {
|
||||
this.attributes = serialized.attributes;
|
||||
}
|
||||
|
||||
data.updateFlags.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method saves/updates an object state in a collection
|
||||
* @method save
|
||||
* @memberof module:API.cvat.classes.ObjectState
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
* @returns {module:API.cvat.classes.ObjectState} updated state of an object
|
||||
*/
|
||||
async save() {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper.call(this, ObjectState.prototype.save);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method deletes an object from a collection
|
||||
* @method delete
|
||||
* @memberof module:API.cvat.classes.ObjectState
|
||||
* @readonly
|
||||
* @instance
|
||||
* @param {boolean} [force=false] delete object even if it is locked
|
||||
* @async
|
||||
* @returns {boolean} true if object has been deleted
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
*/
|
||||
async delete(force = false) {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper.call(this, ObjectState.prototype.delete, force);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the highest ZOrder within a frame
|
||||
* @method up
|
||||
* @memberof module:API.cvat.classes.ObjectState
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
*/
|
||||
async up() {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper.call(this, ObjectState.prototype.up);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the lowest ZOrder within a frame
|
||||
* @method down
|
||||
* @memberof module:API.cvat.classes.ObjectState
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
*/
|
||||
async down() {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper.call(this, ObjectState.prototype.down);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Updates element in collection which contains it
|
||||
ObjectState.prototype.save.implementation = async function () {
|
||||
if (this.hidden && this.hidden.save) {
|
||||
return this.hidden.save();
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
// Delete element from a collection which contains it
|
||||
ObjectState.prototype.delete.implementation = async function (force) {
|
||||
if (this.hidden && this.hidden.delete) {
|
||||
return this.hidden.delete(force);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
ObjectState.prototype.up.implementation = async function () {
|
||||
if (this.hidden && this.hidden.up) {
|
||||
return this.hidden.up();
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
ObjectState.prototype.down.implementation = async function () {
|
||||
if (this.hidden && this.hidden.down) {
|
||||
return this.hidden.down();
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
module.exports = ObjectState;
|
||||
})();
|
||||
@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:false
|
||||
*/
|
||||
|
||||
(() => {
|
||||
const { PluginError } = require('./exceptions');
|
||||
|
||||
const plugins = [];
|
||||
class PluginRegistry {
|
||||
static async apiWrapper(wrappedFunc, ...args) {
|
||||
// I have to optimize the wrapper
|
||||
const pluginList = await PluginRegistry.list();
|
||||
for (const plugin of pluginList) {
|
||||
const pluginDecorators = plugin.functions
|
||||
.filter(obj => obj.callback === wrappedFunc)[0];
|
||||
if (pluginDecorators && pluginDecorators.enter) {
|
||||
try {
|
||||
await pluginDecorators.enter.call(this, plugin, ...args);
|
||||
} catch (exception) {
|
||||
if (exception instanceof PluginError) {
|
||||
throw exception;
|
||||
} else {
|
||||
throw new PluginError(`Exception in plugin ${plugin.name}: ${exception.toString()}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = await wrappedFunc.implementation.call(this, ...args);
|
||||
|
||||
for (const plugin of pluginList) {
|
||||
const pluginDecorators = plugin.functions
|
||||
.filter(obj => obj.callback === wrappedFunc)[0];
|
||||
if (pluginDecorators && pluginDecorators.leave) {
|
||||
try {
|
||||
result = await pluginDecorators.leave.call(this, plugin, result, ...args);
|
||||
} catch (exception) {
|
||||
if (exception instanceof PluginError) {
|
||||
throw exception;
|
||||
} else {
|
||||
throw new PluginError(`Exception in plugin ${plugin.name}: ${exception.toString()}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Called with cvat context
|
||||
static async register(plug) {
|
||||
const functions = [];
|
||||
|
||||
if (typeof (plug) !== 'object') {
|
||||
throw new PluginError(`Plugin should be an object, but got "${typeof (plug)}"`);
|
||||
}
|
||||
|
||||
if (!('name' in plug) || typeof (plug.name) !== 'string') {
|
||||
throw new PluginError('Plugin must contain a "name" field and it must be a string');
|
||||
}
|
||||
|
||||
if (!('description' in plug) || typeof (plug.description) !== 'string') {
|
||||
throw new PluginError('Plugin must contain a "description" field and it must be a string');
|
||||
}
|
||||
|
||||
if ('functions' in plug) {
|
||||
throw new PluginError('Plugin must not contain a "functions" field');
|
||||
}
|
||||
|
||||
(function traverse(plugin, api) {
|
||||
const decorator = {};
|
||||
for (const key in plugin) {
|
||||
if (Object.prototype.hasOwnProperty.call(plugin, key)) {
|
||||
if (typeof (plugin[key]) === 'object') {
|
||||
if (Object.prototype.hasOwnProperty.call(api, key)) {
|
||||
traverse(plugin[key], api[key]);
|
||||
}
|
||||
} else if (['enter', 'leave'].includes(key)
|
||||
&& typeof (api) === 'function'
|
||||
&& typeof (plugin[key] === 'function')) {
|
||||
decorator.callback = api;
|
||||
decorator[key] = plugin[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(decorator).length) {
|
||||
functions.push(decorator);
|
||||
}
|
||||
}(plug, {
|
||||
cvat: this,
|
||||
}));
|
||||
|
||||
Object.defineProperty(plug, 'functions', {
|
||||
value: functions,
|
||||
writable: false,
|
||||
});
|
||||
|
||||
plugins.push(plug);
|
||||
}
|
||||
|
||||
static async list() {
|
||||
return plugins;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PluginRegistry;
|
||||
})();
|
||||
@ -0,0 +1,589 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:false
|
||||
*/
|
||||
|
||||
(() => {
|
||||
const FormData = require('form-data');
|
||||
const {
|
||||
ServerError,
|
||||
} = require('./exceptions');
|
||||
const store = require('store');
|
||||
const config = require('./config');
|
||||
|
||||
function generateError(errorData, baseMessage) {
|
||||
if (errorData.response) {
|
||||
const message = `${baseMessage}. `
|
||||
+ `${errorData.message}. ${JSON.stringify(errorData.response.data) || ''}.`;
|
||||
return new ServerError(message, errorData.response.status);
|
||||
}
|
||||
|
||||
// Server is unavailable (no any response)
|
||||
const message = `${baseMessage}. `
|
||||
+ `${errorData.message}.`; // usually is "Error Network"
|
||||
return new ServerError(message, 0);
|
||||
}
|
||||
|
||||
class ServerProxy {
|
||||
constructor() {
|
||||
const Axios = require('axios');
|
||||
Axios.defaults.withCredentials = true;
|
||||
Axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN';
|
||||
Axios.defaults.xsrfCookieName = 'csrftoken';
|
||||
|
||||
let token = store.get('token');
|
||||
if (token) {
|
||||
Axios.defaults.headers.common.Authorization = `Token ${token}`;
|
||||
}
|
||||
|
||||
async function about() {
|
||||
const { backendAPI } = config;
|
||||
|
||||
let response = null;
|
||||
try {
|
||||
response = await Axios.get(`${backendAPI}/server/about`, {
|
||||
proxy: config.proxy,
|
||||
});
|
||||
} catch (errorData) {
|
||||
throw generateError(errorData, 'Could not get "about" information from the server');
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function share(directory) {
|
||||
const { backendAPI } = config;
|
||||
directory = encodeURIComponent(directory);
|
||||
|
||||
let response = null;
|
||||
try {
|
||||
response = await Axios.get(`${backendAPI}/server/share?directory=${directory}`, {
|
||||
proxy: config.proxy,
|
||||
});
|
||||
} catch (errorData) {
|
||||
throw generateError(errorData, 'Could not get "share" information from the server');
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function exception(exceptionObject) {
|
||||
const { backendAPI } = config;
|
||||
|
||||
try {
|
||||
await Axios.post(`${backendAPI}/server/exception`, JSON.stringify(exceptionObject), {
|
||||
proxy: config.proxy,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
} catch (errorData) {
|
||||
throw generateError(errorData, 'Could not send an exception to the server');
|
||||
}
|
||||
}
|
||||
|
||||
async function formats() {
|
||||
const { backendAPI } = config;
|
||||
|
||||
let response = null;
|
||||
try {
|
||||
response = await Axios.get(`${backendAPI}/server/annotation/formats`, {
|
||||
proxy: config.proxy,
|
||||
});
|
||||
} catch (errorData) {
|
||||
throw generateError(errorData, 'Could not get annotation formats from the server');
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function register(username, firstName, lastName, email, password1, password2) {
|
||||
let response = null;
|
||||
try {
|
||||
const data = JSON.stringify({
|
||||
username,
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
email,
|
||||
password1,
|
||||
password2,
|
||||
});
|
||||
response = await Axios.post(`${config.backendAPI}/auth/register`, data, {
|
||||
proxy: config.proxy,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
} catch (errorData) {
|
||||
throw generateError(errorData, `Could not register '${username}' user on the server`);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function login(username, password) {
|
||||
const authenticationData = ([
|
||||
`${encodeURIComponent('username')}=${encodeURIComponent(username)}`,
|
||||
`${encodeURIComponent('password')}=${encodeURIComponent(password)}`,
|
||||
]).join('&').replace(/%20/g, '+');
|
||||
|
||||
let authenticationResponse = null;
|
||||
try {
|
||||
authenticationResponse = await Axios.post(
|
||||
`${config.backendAPI}/auth/login`,
|
||||
authenticationData, {
|
||||
proxy: config.proxy,
|
||||
},
|
||||
);
|
||||
} catch (errorData) {
|
||||
throw generateError(errorData, 'Could not login on a server');
|
||||
}
|
||||
|
||||
if (authenticationResponse.headers['set-cookie']) {
|
||||
// Browser itself setup cookie and header is none
|
||||
// In NodeJS we need do it manually
|
||||
const cookies = authenticationResponse.headers['set-cookie'].join(';');
|
||||
Axios.defaults.headers.common.Cookie = cookies;
|
||||
}
|
||||
|
||||
token = authenticationResponse.data.key;
|
||||
store.set('token', token);
|
||||
Axios.defaults.headers.common.Authorization = `Token ${token}`;
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await Axios.post(`${config.backendAPI}/auth/logout`, {
|
||||
proxy: config.proxy,
|
||||
});
|
||||
} catch (errorData) {
|
||||
throw generateError(errorData, 'Could not logout from the server');
|
||||
}
|
||||
|
||||
store.remove('token');
|
||||
Axios.defaults.headers.common.Authorization = '';
|
||||
}
|
||||
|
||||
async function authorized() {
|
||||
try {
|
||||
await module.exports.users.getSelf();
|
||||
} catch (serverError) {
|
||||
if (serverError.code === 401) {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw serverError;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function getTasks(filter = '') {
|
||||
const { backendAPI } = config;
|
||||
|
||||
let response = null;
|
||||
try {
|
||||
response = await Axios.get(`${backendAPI}/tasks?${filter}`, {
|
||||
proxy: config.proxy,
|
||||
});
|
||||
} catch (errorData) {
|
||||
throw generateError(errorData, 'Could not get tasks from a server');
|
||||
}
|
||||
|
||||
response.data.results.count = response.data.count;
|
||||
return response.data.results;
|
||||
}
|
||||
|
||||
async function saveTask(id, taskData) {
|
||||
const { backendAPI } = config;
|
||||
|
||||
try {
|
||||
await Axios.patch(`${backendAPI}/tasks/${id}`, JSON.stringify(taskData), {
|
||||
proxy: config.proxy,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
} catch (errorData) {
|
||||
throw generateError(errorData, 'Could not save the task on the server');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTask(id) {
|
||||
const { backendAPI } = config;
|
||||
|
||||
try {
|
||||
await Axios.delete(`${backendAPI}/tasks/${id}`);
|
||||
} catch (errorData) {
|
||||
throw generateError(errorData, 'Could not delete the task from the server');
|
||||
}
|
||||
}
|
||||
|
||||
async function createTask(taskData, files, onUpdate) {
|
||||
const { backendAPI } = config;
|
||||
|
||||
async function wait(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
async function checkStatus() {
|
||||
try {
|
||||
const response = await Axios.get(`${backendAPI}/tasks/${id}/status`);
|
||||
if (['Queued', 'Started'].includes(response.data.state)) {
|
||||
if (response.data.message !== '') {
|
||||
onUpdate(response.data.message);
|
||||
}
|
||||
setTimeout(checkStatus, 1000);
|
||||
} else if (response.data.state === 'Finished') {
|
||||
resolve();
|
||||
} else if (response.data.state === 'Failed') {
|
||||
// If request has been successful, but task hasn't been created
|
||||
// Then passed data is wrong and we can pass code 400
|
||||
const message = 'Could not create the task on the server. '
|
||||
+ `${response.data.message}.`;
|
||||
reject(new ServerError(message, 400));
|
||||
} else {
|
||||
// If server has another status, it is unexpected
|
||||
// Therefore it is server error and we can pass code 500
|
||||
reject(new ServerError(
|
||||
`Unknown task state has been received: ${response.data.state}`,
|
||||
500,
|
||||
));
|
||||
}
|
||||
} catch (errorData) {
|
||||
reject(
|
||||
generateError(errorData, 'Could not put task to the server'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(checkStatus, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
const batchOfFiles = new FormData();
|
||||
for (const key in files) {
|
||||
if (Object.prototype.hasOwnProperty.call(files, key)) {
|
||||
for (let i = 0; i < files[key].length; i++) {
|
||||
batchOfFiles.append(`${key}[${i}]`, files[key][i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let response = null;
|
||||
|
||||
onUpdate('The task is being created on the server..');
|
||||
try {
|
||||
response = await Axios.post(`${backendAPI}/tasks`, JSON.stringify(taskData), {
|
||||
proxy: config.proxy,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
} catch (errorData) {
|
||||
throw generateError(errorData, 'Could not put task to the server');
|
||||
}
|
||||
|
||||
onUpdate('The data is being uploaded to the server..');
|
||||
try {
|
||||
await Axios.post(`${backendAPI}/tasks/${response.data.id}/data`, batchOfFiles, {
|
||||
proxy: config.proxy,
|
||||
});
|
||||
} catch (errorData) {
|
||||
try {
|
||||
await deleteTask(response.data.id);
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
throw generateError(errorData, 'Could not put data to the server');
|
||||
}
|
||||
|
||||
try {
|
||||
await wait(response.data.id);
|
||||
} catch (createException) {
|
||||
await deleteTask(response.data.id);
|
||||
throw createException;
|
||||
}
|
||||
|
||||
const createdTask = await getTasks(`?id=${response.id}`);
|
||||
return createdTask[0];
|
||||
}
|
||||
|
||||
async function getJob(jobID) {
|
||||
const { backendAPI } = config;
|
||||
|
||||
let response = null;
|
||||
try {
|
||||
response = await Axios.get(`${backendAPI}/jobs/${jobID}`, {
|
||||
proxy: config.proxy,
|
||||
});
|
||||
} catch (errorData) {
|
||||
throw generateError(errorData, 'Could not get jobs from a server');
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function saveJob(id, jobData) {
|
||||
const { backendAPI } = config;
|
||||
|
||||
try {
|
||||
await Axios.patch(`${backendAPI}/jobs/${id}`, JSON.stringify(jobData), {
|
||||
proxy: config.proxy,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
} catch (errorData) {
|
||||
throw generateError(errorData, 'Could not save the job on the server');
|
||||
}
|
||||
}
|
||||
|
||||
async function getUsers() {
|
||||
const { backendAPI } = config;
|
||||
|
||||
let response = null;
|
||||
try {
|
||||
response = await Axios.get(`${backendAPI}/users`, {
|
||||
proxy: config.proxy,
|
||||
});
|
||||
} catch (errorData) {
|
||||
throw generateError(errorData, 'Could not get users from the server');
|
||||
}
|
||||
|
||||
return response.data.results;
|
||||
}
|
||||
|
||||
async function getSelf() {
|
||||
const { backendAPI } = config;
|
||||
|
||||
let response = null;
|
||||
try {
|
||||
response = await Axios.get(`${backendAPI}/users/self`, {
|
||||
proxy: config.proxy,
|
||||
});
|
||||
} catch (errorData) {
|
||||
throw generateError(errorData, 'Could not get user data from the server');
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function getData(tid, frame) {
|
||||
const { backendAPI } = config;
|
||||
|
||||
let response = null;
|
||||
try {
|
||||
response = await Axios.get(`${backendAPI}/tasks/${tid}/frames/${frame}`, {
|
||||
proxy: config.proxy,
|
||||
responseType: 'blob',
|
||||
});
|
||||
} catch (errorData) {
|
||||
throw generateError(
|
||||
errorData,
|
||||
`Could not get frame ${frame} for the task ${tid} from the server`,
|
||||
);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function getMeta(tid) {
|
||||
const { backendAPI } = config;
|
||||
|
||||
let response = null;
|
||||
try {
|
||||
response = await Axios.get(`${backendAPI}/tasks/${tid}/frames/meta`, {
|
||||
proxy: config.proxy,
|
||||
});
|
||||
} catch (errorData) {
|
||||
throw generateError(
|
||||
errorData,
|
||||
`Could not get frame meta info for the task ${tid} from the server`,
|
||||
);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Session is 'task' or 'job'
|
||||
async function getAnnotations(session, id) {
|
||||
const { backendAPI } = config;
|
||||
|
||||
let response = null;
|
||||
try {
|
||||
response = await Axios.get(`${backendAPI}/${session}s/${id}/annotations`, {
|
||||
proxy: config.proxy,
|
||||
});
|
||||
} catch (errorData) {
|
||||
throw generateError(
|
||||
errorData,
|
||||
`Could not get annotations for the ${session} ${id} from the server`,
|
||||
);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Session is 'task' or 'job'
|
||||
async function updateAnnotations(session, id, data, action) {
|
||||
const { backendAPI } = config;
|
||||
let requestFunc = null;
|
||||
let url = null;
|
||||
if (action.toUpperCase() === 'PUT') {
|
||||
requestFunc = Axios.put.bind(Axios);
|
||||
url = `${backendAPI}/${session}s/${id}/annotations`;
|
||||
} else {
|
||||
requestFunc = Axios.patch.bind(Axios);
|
||||
url = `${backendAPI}/${session}s/${id}/annotations?action=${action}`;
|
||||
}
|
||||
|
||||
let response = null;
|
||||
try {
|
||||
response = await requestFunc(url, JSON.stringify(data), {
|
||||
proxy: config.proxy,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
} catch (errorData) {
|
||||
throw generateError(
|
||||
errorData,
|
||||
`Could not ${action} annotations for the ${session} ${id} on the server`,
|
||||
);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Session is 'task' or 'job'
|
||||
async function uploadAnnotations(session, id, file, format) {
|
||||
const { backendAPI } = config;
|
||||
|
||||
let annotationData = new FormData();
|
||||
annotationData.append('annotation_file', file);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
async function request() {
|
||||
try {
|
||||
const response = await Axios
|
||||
.put(`${backendAPI}/${session}s/${id}/annotations?format=${format}`, annotationData, {
|
||||
proxy: config.proxy,
|
||||
});
|
||||
if (response.status === 202) {
|
||||
annotationData = new FormData();
|
||||
setTimeout(request, 3000);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
} catch (errorData) {
|
||||
reject(generateError(
|
||||
errorData,
|
||||
`Could not upload annotations for the ${session} ${id}`,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(request);
|
||||
});
|
||||
}
|
||||
|
||||
// Session is 'task' or 'job'
|
||||
async function dumpAnnotations(id, name, format) {
|
||||
const { backendAPI } = config;
|
||||
const filename = name.replace(/\//g, '_');
|
||||
let url = `${backendAPI}/tasks/${id}/annotations/${filename}?format=${format}`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
async function request() {
|
||||
try {
|
||||
const response = await Axios
|
||||
.get(`${url}`, {
|
||||
proxy: config.proxy,
|
||||
});
|
||||
if (response.status === 202) {
|
||||
setTimeout(request, 3000);
|
||||
} else {
|
||||
url = `${url}&action=download`;
|
||||
resolve(url);
|
||||
}
|
||||
} catch (errorData) {
|
||||
reject(generateError(
|
||||
errorData,
|
||||
`Could not dump annotations for the task ${id} from the server`,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(request);
|
||||
});
|
||||
}
|
||||
|
||||
Object.defineProperties(this, Object.freeze({
|
||||
server: {
|
||||
value: Object.freeze({
|
||||
about,
|
||||
share,
|
||||
formats,
|
||||
exception,
|
||||
login,
|
||||
logout,
|
||||
authorized,
|
||||
register,
|
||||
}),
|
||||
writable: false,
|
||||
},
|
||||
|
||||
tasks: {
|
||||
value: Object.freeze({
|
||||
getTasks,
|
||||
saveTask,
|
||||
createTask,
|
||||
deleteTask,
|
||||
}),
|
||||
writable: false,
|
||||
},
|
||||
|
||||
jobs: {
|
||||
value: Object.freeze({
|
||||
getJob,
|
||||
saveJob,
|
||||
}),
|
||||
writable: false,
|
||||
},
|
||||
|
||||
users: {
|
||||
value: Object.freeze({
|
||||
getUsers,
|
||||
getSelf,
|
||||
}),
|
||||
writable: false,
|
||||
},
|
||||
|
||||
frames: {
|
||||
value: Object.freeze({
|
||||
getData,
|
||||
getMeta,
|
||||
}),
|
||||
writable: false,
|
||||
},
|
||||
|
||||
annotations: {
|
||||
value: Object.freeze({
|
||||
updateAnnotations,
|
||||
getAnnotations,
|
||||
dumpAnnotations,
|
||||
uploadAnnotations,
|
||||
}),
|
||||
writable: false,
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
const serverProxy = new ServerProxy();
|
||||
module.exports = serverProxy;
|
||||
})();
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
|
||||
(() => {
|
||||
/**
|
||||
* Class representing collection statistics
|
||||
* @memberof module:API.cvat.classes
|
||||
* @hideconstructor
|
||||
*/
|
||||
class Statistics {
|
||||
constructor(label, total) {
|
||||
Object.defineProperties(this, Object.freeze({
|
||||
/**
|
||||
* Statistics by labels with a structure:
|
||||
* @example
|
||||
* {
|
||||
* label: {
|
||||
* boxes: {
|
||||
* tracks: 10,
|
||||
* shapes: 11,
|
||||
* },
|
||||
* polygons: {
|
||||
* tracks: 13,
|
||||
* shapes: 14,
|
||||
* },
|
||||
* polylines: {
|
||||
* tracks: 16,
|
||||
* shapes: 17,
|
||||
* },
|
||||
* points: {
|
||||
* tracks: 19,
|
||||
* shapes: 20,
|
||||
* },
|
||||
* tags: 66,
|
||||
* manually: 186,
|
||||
* interpolated: 500,
|
||||
* total: 608,
|
||||
* }
|
||||
* }
|
||||
* @name label
|
||||
* @type {Object}
|
||||
* @memberof module:API.cvat.classes.Statistics
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
label: {
|
||||
get: () => JSON.parse(JSON.stringify(label)),
|
||||
},
|
||||
/**
|
||||
* Total statistics (covers all labels) with a structure:
|
||||
* @example
|
||||
* {
|
||||
* boxes: {
|
||||
* tracks: 10,
|
||||
* shapes: 11,
|
||||
* },
|
||||
* polygons: {
|
||||
* tracks: 13,
|
||||
* shapes: 14,
|
||||
* },
|
||||
* polylines: {
|
||||
* tracks: 16,
|
||||
* shapes: 17,
|
||||
* },
|
||||
* points: {
|
||||
* tracks: 19,
|
||||
* shapes: 20,
|
||||
* },
|
||||
* tags: 66,
|
||||
* manually: 186,
|
||||
* interpolated: 500,
|
||||
* total: 608,
|
||||
* }
|
||||
* @name total
|
||||
* @type {Object}
|
||||
* @memberof module:API.cvat.classes.Statistics
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
total: {
|
||||
get: () => JSON.parse(JSON.stringify(total)),
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Statistics;
|
||||
})();
|
||||
@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
(() => {
|
||||
/**
|
||||
* Class representing a user
|
||||
* @memberof module:API.cvat.classes
|
||||
* @hideconstructor
|
||||
*/
|
||||
class User {
|
||||
constructor(initialData) {
|
||||
const data = {
|
||||
id: null,
|
||||
username: null,
|
||||
email: null,
|
||||
first_name: null,
|
||||
last_name: null,
|
||||
groups: null,
|
||||
last_login: null,
|
||||
date_joined: null,
|
||||
is_staff: null,
|
||||
is_superuser: null,
|
||||
is_active: null,
|
||||
};
|
||||
|
||||
for (const property in data) {
|
||||
if (Object.prototype.hasOwnProperty.call(data, property)
|
||||
&& property in initialData) {
|
||||
data[property] = initialData[property];
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperties(this, Object.freeze({
|
||||
id: {
|
||||
/**
|
||||
* @name id
|
||||
* @type {integer}
|
||||
* @memberof module:API.cvat.classes.User
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.id,
|
||||
},
|
||||
username: {
|
||||
/**
|
||||
* @name username
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.User
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.username,
|
||||
},
|
||||
email: {
|
||||
/**
|
||||
* @name email
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.User
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.email,
|
||||
},
|
||||
firstName: {
|
||||
/**
|
||||
* @name firstName
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.User
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.first_name,
|
||||
},
|
||||
lastName: {
|
||||
/**
|
||||
* @name lastName
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.User
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.last_name,
|
||||
},
|
||||
groups: {
|
||||
/**
|
||||
* @name groups
|
||||
* @type {string[]}
|
||||
* @memberof module:API.cvat.classes.User
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => JSON.parse(JSON.stringify(data.groups)),
|
||||
},
|
||||
lastLogin: {
|
||||
/**
|
||||
* @name lastLogin
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.User
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.last_login,
|
||||
},
|
||||
dateJoined: {
|
||||
/**
|
||||
* @name dateJoined
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.User
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.date_joined,
|
||||
},
|
||||
isStaff: {
|
||||
/**
|
||||
* @name isStaff
|
||||
* @type {boolean}
|
||||
* @memberof module:API.cvat.classes.User
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.is_staff,
|
||||
},
|
||||
isSuperuser: {
|
||||
/**
|
||||
* @name isSuperuser
|
||||
* @type {boolean}
|
||||
* @memberof module:API.cvat.classes.User
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.is_superuser,
|
||||
},
|
||||
isActive: {
|
||||
/**
|
||||
* @name isActive
|
||||
* @type {boolean}
|
||||
* @memberof module:API.cvat.classes.User
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
get: () => data.is_active,
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = User;
|
||||
})();
|
||||
@ -0,0 +1,710 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:false
|
||||
jest:false
|
||||
describe:false
|
||||
*/
|
||||
|
||||
// Setup mock for a server
|
||||
jest.mock('../../src/server-proxy', () => {
|
||||
const mock = require('../mocks/server-proxy.mock');
|
||||
return mock;
|
||||
});
|
||||
|
||||
// Initialize api
|
||||
window.cvat = require('../../src/api');
|
||||
const serverProxy = require('../../src/server-proxy');
|
||||
|
||||
// Test cases
|
||||
describe('Feature: get annotations', () => {
|
||||
test('get annotations from a task', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
const annotations = await task.annotations.get(0);
|
||||
expect(Array.isArray(annotations)).toBeTruthy();
|
||||
expect(annotations).toHaveLength(11);
|
||||
for (const state of annotations) {
|
||||
expect(state).toBeInstanceOf(window.cvat.classes.ObjectState);
|
||||
}
|
||||
});
|
||||
|
||||
test('get annotations from a job', async () => {
|
||||
const job = (await window.cvat.jobs.get({ jobID: 101 }))[0];
|
||||
const annotations0 = await job.annotations.get(0);
|
||||
const annotations10 = await job.annotations.get(10);
|
||||
expect(Array.isArray(annotations0)).toBeTruthy();
|
||||
expect(Array.isArray(annotations10)).toBeTruthy();
|
||||
expect(annotations0).toHaveLength(1);
|
||||
expect(annotations10).toHaveLength(2);
|
||||
for (const state of annotations0.concat(annotations10)) {
|
||||
expect(state).toBeInstanceOf(window.cvat.classes.ObjectState);
|
||||
}
|
||||
});
|
||||
|
||||
test('get annotations for frame out of task', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
|
||||
// Out of task
|
||||
expect(task.annotations.get(500))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
|
||||
// Out of task
|
||||
expect(task.annotations.get(-1))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('get annotations for frame out of job', async () => {
|
||||
const job = (await window.cvat.jobs.get({ jobID: 101 }))[0];
|
||||
|
||||
// Out of segment
|
||||
expect(job.annotations.get(500))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
|
||||
// Out of segment
|
||||
expect(job.annotations.get(-1))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
// TODO: Test filter (hasn't been implemented yet)
|
||||
});
|
||||
|
||||
|
||||
describe('Feature: put annotations', () => {
|
||||
test('put a shape to a task', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
|
||||
let annotations = await task.annotations.get(1);
|
||||
const { length } = annotations;
|
||||
|
||||
const state = new window.cvat.classes.ObjectState({
|
||||
frame: 1,
|
||||
objectType: window.cvat.enums.ObjectType.SHAPE,
|
||||
shapeType: window.cvat.enums.ObjectShape.POLYGON,
|
||||
points: [0, 0, 100, 0, 100, 50],
|
||||
occluded: true,
|
||||
label: task.labels[0],
|
||||
});
|
||||
|
||||
await task.annotations.put([state]);
|
||||
annotations = await task.annotations.get(1);
|
||||
expect(annotations).toHaveLength(length + 1);
|
||||
});
|
||||
|
||||
test('put a shape to a job', async () => {
|
||||
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
|
||||
let annotations = await job.annotations.get(5);
|
||||
const { length } = annotations;
|
||||
|
||||
const state = new window.cvat.classes.ObjectState({
|
||||
frame: 5,
|
||||
objectType: window.cvat.enums.ObjectType.SHAPE,
|
||||
shapeType: window.cvat.enums.ObjectShape.RECTANGLE,
|
||||
points: [0, 0, 100, 100],
|
||||
occluded: false,
|
||||
label: job.task.labels[0],
|
||||
});
|
||||
|
||||
await job.annotations.put([state]);
|
||||
annotations = await job.annotations.get(5);
|
||||
expect(annotations).toHaveLength(length + 1);
|
||||
});
|
||||
|
||||
test('put a track to a task', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
|
||||
let annotations = await task.annotations.get(1);
|
||||
const { length } = annotations;
|
||||
|
||||
const state = new window.cvat.classes.ObjectState({
|
||||
frame: 1,
|
||||
objectType: window.cvat.enums.ObjectType.TRACK,
|
||||
shapeType: window.cvat.enums.ObjectShape.POLYGON,
|
||||
points: [0, 0, 100, 0, 100, 50],
|
||||
occluded: true,
|
||||
label: task.labels[0],
|
||||
});
|
||||
|
||||
await task.annotations.put([state]);
|
||||
annotations = await task.annotations.get(1);
|
||||
expect(annotations).toHaveLength(length + 1);
|
||||
});
|
||||
|
||||
test('put a track to a job', async () => {
|
||||
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
|
||||
let annotations = await job.annotations.get(5);
|
||||
const { length } = annotations;
|
||||
|
||||
const state = new window.cvat.classes.ObjectState({
|
||||
frame: 5,
|
||||
objectType: window.cvat.enums.ObjectType.TRACK,
|
||||
shapeType: window.cvat.enums.ObjectShape.RECTANGLE,
|
||||
points: [0, 0, 100, 100],
|
||||
occluded: false,
|
||||
label: job.task.labels[0],
|
||||
});
|
||||
|
||||
await job.annotations.put([state]);
|
||||
annotations = await job.annotations.get(5);
|
||||
expect(annotations).toHaveLength(length + 1);
|
||||
});
|
||||
|
||||
test('put object without objectType to a task', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
|
||||
await task.annotations.clear(true);
|
||||
const state = new window.cvat.classes.ObjectState({
|
||||
frame: 1,
|
||||
shapeType: window.cvat.enums.ObjectShape.POLYGON,
|
||||
points: [0, 0, 100, 0, 100, 50],
|
||||
occluded: true,
|
||||
label: task.labels[0],
|
||||
});
|
||||
|
||||
expect(task.annotations.put([state]))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('put shape with bad attributes to a task', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
|
||||
await task.annotations.clear(true);
|
||||
const state = new window.cvat.classes.ObjectState({
|
||||
frame: 1,
|
||||
objectType: window.cvat.enums.ObjectType.SHAPE,
|
||||
shapeType: window.cvat.enums.ObjectShape.POLYGON,
|
||||
points: [0, 0, 100, 0, 100, 50],
|
||||
attributes: { 'bad key': 55 },
|
||||
occluded: true,
|
||||
label: task.labels[0],
|
||||
});
|
||||
|
||||
expect(task.annotations.put([state]))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('put shape without points and with invalud points to a task', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
|
||||
await task.annotations.clear(true);
|
||||
const state = new window.cvat.classes.ObjectState({
|
||||
frame: 1,
|
||||
objectType: window.cvat.enums.ObjectType.SHAPE,
|
||||
shapeType: window.cvat.enums.ObjectShape.POLYGON,
|
||||
occluded: true,
|
||||
points: [],
|
||||
label: task.labels[0],
|
||||
});
|
||||
|
||||
await expect(task.annotations.put([state]))
|
||||
.rejects.toThrow(window.cvat.exceptions.DataError);
|
||||
|
||||
delete state.points;
|
||||
await expect(task.annotations.put([state]))
|
||||
.rejects.toThrow(window.cvat.exceptions.DataError);
|
||||
|
||||
state.points = ['150,50 250,30'];
|
||||
expect(task.annotations.put([state]))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('put shape without type to a task', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
|
||||
await task.annotations.clear(true);
|
||||
const state = new window.cvat.classes.ObjectState({
|
||||
frame: 1,
|
||||
objectType: window.cvat.enums.ObjectType.SHAPE,
|
||||
points: [0, 0, 100, 0, 100, 50],
|
||||
occluded: true,
|
||||
label: task.labels[0],
|
||||
});
|
||||
|
||||
expect(task.annotations.put([state]))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('put shape without label and with bad label to a task', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
|
||||
await task.annotations.clear(true);
|
||||
const state = new window.cvat.classes.ObjectState({
|
||||
frame: 1,
|
||||
objectType: window.cvat.enums.ObjectType.SHAPE,
|
||||
shapeType: window.cvat.enums.ObjectShape.POLYGON,
|
||||
points: [0, 0, 100, 0, 100, 50],
|
||||
occluded: true,
|
||||
});
|
||||
|
||||
await expect(task.annotations.put([state]))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
|
||||
state.label = 'bad label';
|
||||
await expect(task.annotations.put([state]))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
|
||||
state.label = {};
|
||||
await expect(task.annotations.put([state]))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('put shape with bad frame to a task', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
|
||||
await task.annotations.clear(true);
|
||||
const state = new window.cvat.classes.ObjectState({
|
||||
frame: '5',
|
||||
objectType: window.cvat.enums.ObjectType.SHAPE,
|
||||
shapeType: window.cvat.enums.ObjectShape.POLYGON,
|
||||
points: [0, 0, 100, 0, 100, 50],
|
||||
occluded: true,
|
||||
label: task.labels[0],
|
||||
});
|
||||
|
||||
expect(task.annotations.put([state]))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Feature: check unsaved changes', () => {
|
||||
test('check unsaved changes in a task', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
|
||||
expect(await task.annotations.hasUnsavedChanges()).toBe(false);
|
||||
const annotations = await task.annotations.get(0);
|
||||
|
||||
annotations[0].keyframe = true;
|
||||
await annotations[0].save();
|
||||
|
||||
expect(await task.annotations.hasUnsavedChanges()).toBe(true);
|
||||
});
|
||||
|
||||
test('check unsaved changes in a job', async () => {
|
||||
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
|
||||
expect(await job.annotations.hasUnsavedChanges()).toBe(false);
|
||||
const annotations = await job.annotations.get(0);
|
||||
|
||||
annotations[0].occluded = true;
|
||||
await annotations[0].save();
|
||||
|
||||
expect(await job.annotations.hasUnsavedChanges()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Feature: save annotations', () => {
|
||||
test('create & save annotations for a task', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
|
||||
let annotations = await task.annotations.get(0);
|
||||
const { length } = annotations;
|
||||
const state = new window.cvat.classes.ObjectState({
|
||||
frame: 0,
|
||||
objectType: window.cvat.enums.ObjectType.SHAPE,
|
||||
shapeType: window.cvat.enums.ObjectShape.POLYGON,
|
||||
points: [0, 0, 100, 0, 100, 50],
|
||||
occluded: true,
|
||||
label: task.labels[0],
|
||||
});
|
||||
|
||||
expect(await task.annotations.hasUnsavedChanges()).toBe(false);
|
||||
await task.annotations.put([state]);
|
||||
expect(await task.annotations.hasUnsavedChanges()).toBe(true);
|
||||
await task.annotations.save();
|
||||
expect(await task.annotations.hasUnsavedChanges()).toBe(false);
|
||||
annotations = await task.annotations.get(0);
|
||||
expect(annotations).toHaveLength(length + 1);
|
||||
});
|
||||
|
||||
test('update & save annotations for a task', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
|
||||
const annotations = await task.annotations.get(0);
|
||||
|
||||
expect(await task.annotations.hasUnsavedChanges()).toBe(false);
|
||||
annotations[0].occluded = true;
|
||||
await annotations[0].save();
|
||||
expect(await task.annotations.hasUnsavedChanges()).toBe(true);
|
||||
await task.annotations.save();
|
||||
expect(await task.annotations.hasUnsavedChanges()).toBe(false);
|
||||
});
|
||||
|
||||
test('delete & save annotations for a task', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
|
||||
const annotations = await task.annotations.get(0);
|
||||
|
||||
expect(await task.annotations.hasUnsavedChanges()).toBe(false);
|
||||
await annotations[0].delete();
|
||||
expect(await task.annotations.hasUnsavedChanges()).toBe(true);
|
||||
await task.annotations.save();
|
||||
expect(await task.annotations.hasUnsavedChanges()).toBe(false);
|
||||
});
|
||||
|
||||
test('create & save annotations for a job', async () => {
|
||||
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
|
||||
let annotations = await job.annotations.get(0);
|
||||
const { length } = annotations;
|
||||
const state = new window.cvat.classes.ObjectState({
|
||||
frame: 0,
|
||||
objectType: window.cvat.enums.ObjectType.SHAPE,
|
||||
shapeType: window.cvat.enums.ObjectShape.POLYGON,
|
||||
points: [0, 0, 100, 0, 100, 50],
|
||||
occluded: true,
|
||||
label: job.task.labels[0],
|
||||
});
|
||||
|
||||
expect(await job.annotations.hasUnsavedChanges()).toBe(false);
|
||||
await job.annotations.put([state]);
|
||||
expect(await job.annotations.hasUnsavedChanges()).toBe(true);
|
||||
await job.annotations.save();
|
||||
expect(await job.annotations.hasUnsavedChanges()).toBe(false);
|
||||
annotations = await job.annotations.get(0);
|
||||
expect(annotations).toHaveLength(length + 1);
|
||||
});
|
||||
|
||||
test('update & save annotations for a job', async () => {
|
||||
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
|
||||
const annotations = await job.annotations.get(0);
|
||||
|
||||
expect(await job.annotations.hasUnsavedChanges()).toBe(false);
|
||||
annotations[0].points = [0, 100, 200, 300];
|
||||
await annotations[0].save();
|
||||
expect(await job.annotations.hasUnsavedChanges()).toBe(true);
|
||||
await job.annotations.save();
|
||||
expect(await job.annotations.hasUnsavedChanges()).toBe(false);
|
||||
});
|
||||
|
||||
test('delete & save annotations for a job', async () => {
|
||||
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
|
||||
const annotations = await job.annotations.get(0);
|
||||
|
||||
expect(await job.annotations.hasUnsavedChanges()).toBe(false);
|
||||
await annotations[0].delete();
|
||||
expect(await job.annotations.hasUnsavedChanges()).toBe(true);
|
||||
await job.annotations.save();
|
||||
expect(await job.annotations.hasUnsavedChanges()).toBe(false);
|
||||
});
|
||||
|
||||
test('delete & save annotations for a job when there are a track and a shape with the same id', async () => {
|
||||
const job = (await window.cvat.jobs.get({ jobID: 112 }))[0];
|
||||
const annotations = await job.annotations.get(0);
|
||||
let okay = false;
|
||||
|
||||
// Temporary override this method because we need to know what data
|
||||
// have been sent to a server
|
||||
const oldImplementation = serverProxy.annotations.updateAnnotations;
|
||||
serverProxy.annotations.updateAnnotations = async (session, id, data, action) => {
|
||||
const result = await oldImplementation
|
||||
.call(serverProxy.annotations, session, id, data, action);
|
||||
if (action === 'delete') {
|
||||
okay = okay || (action === 'delete' && !!(data.shapes.length || data.tracks.length));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
await annotations[0].delete();
|
||||
await job.annotations.save();
|
||||
|
||||
serverProxy.annotations.updateAnnotations = oldImplementation;
|
||||
expect(okay).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Feature: merge annotations', () => {
|
||||
test('merge annotations in a task', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
const annotations0 = await task.annotations.get(0);
|
||||
const annotations1 = await task.annotations.get(1);
|
||||
const states = [annotations0[0], annotations1[0]];
|
||||
await task.annotations.merge(states);
|
||||
const merged0 = (await task.annotations.get(0))
|
||||
.filter((state) => state.objectType === window.cvat.enums.ObjectType.TRACK);
|
||||
const merged1 = (await task.annotations.get(1))
|
||||
.filter((state) => state.objectType === window.cvat.enums.ObjectType.TRACK);
|
||||
expect(merged0).toHaveLength(1);
|
||||
expect(merged1).toHaveLength(1);
|
||||
|
||||
expect(merged0[0].points).toEqual(states[0].points);
|
||||
expect(merged1[0].points).toEqual(states[1].points);
|
||||
});
|
||||
|
||||
test('merge annotations in a job', async () => {
|
||||
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
|
||||
const annotations0 = await job.annotations.get(0);
|
||||
const annotations1 = await job.annotations.get(1);
|
||||
const states = [annotations0[0], annotations1[0]];
|
||||
await job.annotations.merge(states);
|
||||
const merged0 = (await job.annotations.get(0))
|
||||
.filter((state) => state.objectType === window.cvat.enums.ObjectType.TRACK);
|
||||
const merged1 = (await job.annotations.get(1))
|
||||
.filter((state) => state.objectType === window.cvat.enums.ObjectType.TRACK);
|
||||
expect(merged0).toHaveLength(1);
|
||||
expect(merged1).toHaveLength(1);
|
||||
|
||||
expect(merged0[0].points).toEqual(states[0].points);
|
||||
expect(merged1[0].points).toEqual(states[1].points);
|
||||
});
|
||||
|
||||
test('trying to merge not object state', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
const annotations0 = await task.annotations.get(0);
|
||||
const states = [annotations0[0], {}];
|
||||
|
||||
expect(task.annotations.merge(states))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('trying to merge object state which is not saved in a collection', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
const annotations0 = await task.annotations.get(0);
|
||||
|
||||
const state = new window.cvat.classes.ObjectState({
|
||||
frame: 0,
|
||||
objectType: window.cvat.enums.ObjectType.SHAPE,
|
||||
shapeType: window.cvat.enums.ObjectShape.POLYGON,
|
||||
points: [0, 0, 100, 0, 100, 50],
|
||||
occluded: true,
|
||||
label: task.labels[0],
|
||||
});
|
||||
const states = [annotations0[0], state];
|
||||
|
||||
expect(task.annotations.merge(states))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('trying to merge with bad label', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
const annotations0 = await task.annotations.get(0);
|
||||
const annotations1 = await task.annotations.get(1);
|
||||
const states = [annotations0[0], annotations1[0]];
|
||||
states[0].label = new window.cvat.classes.Label({
|
||||
id: 500,
|
||||
name: 'new_label',
|
||||
attributes: [],
|
||||
});
|
||||
|
||||
expect(task.annotations.merge(states))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('trying to merge with different shape types', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
const annotations0 = await task.annotations.get(0);
|
||||
const annotations1 = (await task.annotations.get(1))
|
||||
.filter((state) => state.shapeType === window.cvat.enums.ObjectShape.POLYGON);
|
||||
const states = [annotations0[0], annotations1[0]];
|
||||
|
||||
expect(task.annotations.merge(states))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('trying to merge with different labels', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
const annotations0 = await task.annotations.get(0);
|
||||
const annotations1 = await task.annotations.get(1);
|
||||
const states = [annotations0[0], annotations1[0]];
|
||||
states[1].label = new window.cvat.classes.Label({
|
||||
id: 500,
|
||||
name: 'new_label',
|
||||
attributes: [],
|
||||
});
|
||||
|
||||
expect(task.annotations.merge(states))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Feature: split annotations', () => {
|
||||
test('split annotations in a task', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
|
||||
const annotations4 = await task.annotations.get(4);
|
||||
const annotations5 = await task.annotations.get(5);
|
||||
|
||||
expect(annotations4[0].clientID).toBe(annotations5[0].clientID);
|
||||
await task.annotations.split(annotations5[0], 5);
|
||||
const splitted4 = await task.annotations.get(4);
|
||||
const splitted5 = (await task.annotations.get(5)).filter((state) => !state.outside);
|
||||
expect(splitted4[0].clientID).not.toBe(splitted5[0].clientID);
|
||||
});
|
||||
|
||||
test('split annotations in a job', async () => {
|
||||
const job = (await window.cvat.jobs.get({ jobID: 101 }))[0];
|
||||
const annotations4 = await job.annotations.get(4);
|
||||
const annotations5 = await job.annotations.get(5);
|
||||
|
||||
expect(annotations4[0].clientID).toBe(annotations5[0].clientID);
|
||||
await job.annotations.split(annotations5[0], 5);
|
||||
const splitted4 = await job.annotations.get(4);
|
||||
const splitted5 = (await job.annotations.get(5)).filter((state) => !state.outside);
|
||||
expect(splitted4[0].clientID).not.toBe(splitted5[0].clientID);
|
||||
});
|
||||
|
||||
test('split on a bad frame', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
|
||||
const annotations4 = await task.annotations.get(4);
|
||||
const annotations5 = await task.annotations.get(5);
|
||||
|
||||
expect(annotations4[0].clientID).toBe(annotations5[0].clientID);
|
||||
expect(task.annotations.split(annotations5[0], 'bad frame'))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Feature: group annotations', () => {
|
||||
test('group annotations in a task', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
let annotations = await task.annotations.get(0);
|
||||
const groupID = await task.annotations.group(annotations);
|
||||
expect(typeof (groupID)).toBe('number');
|
||||
annotations = await task.annotations.get(0);
|
||||
for (const state of annotations) {
|
||||
expect(state.group).toBe(groupID);
|
||||
}
|
||||
});
|
||||
|
||||
test('group annotations in a job', async () => {
|
||||
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
|
||||
let annotations = await job.annotations.get(0);
|
||||
const groupID = await job.annotations.group(annotations);
|
||||
expect(typeof (groupID)).toBe('number');
|
||||
annotations = await job.annotations.get(0);
|
||||
for (const state of annotations) {
|
||||
expect(state.group).toBe(groupID);
|
||||
}
|
||||
});
|
||||
|
||||
test('trying to group object state which has not been saved in a collection', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
await task.annotations.clear(true);
|
||||
|
||||
const state = new window.cvat.classes.ObjectState({
|
||||
frame: 0,
|
||||
objectType: window.cvat.enums.ObjectType.SHAPE,
|
||||
shapeType: window.cvat.enums.ObjectShape.POLYGON,
|
||||
points: [0, 0, 100, 0, 100, 50],
|
||||
occluded: true,
|
||||
label: task.labels[0],
|
||||
});
|
||||
|
||||
expect(task.annotations.group([state]))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('trying to group not object state', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
const annotations = await task.annotations.get(0);
|
||||
expect(task.annotations.group(annotations.concat({})))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Feature: clear annotations', () => {
|
||||
test('clear annotations in a task', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
let annotations = await task.annotations.get(0);
|
||||
expect(annotations.length).not.toBe(0);
|
||||
await task.annotations.clear();
|
||||
annotations = await task.annotations.get(0);
|
||||
expect(annotations.length).toBe(0);
|
||||
});
|
||||
|
||||
test('clear annotations in a job', async () => {
|
||||
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
|
||||
let annotations = await job.annotations.get(0);
|
||||
expect(annotations.length).not.toBe(0);
|
||||
await job.annotations.clear();
|
||||
annotations = await job.annotations.get(0);
|
||||
expect(annotations.length).toBe(0);
|
||||
});
|
||||
|
||||
test('clear annotations with reload in a task', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
let annotations = await task.annotations.get(0);
|
||||
expect(annotations.length).not.toBe(0);
|
||||
annotations[0].occluded = true;
|
||||
await annotations[0].save();
|
||||
expect(await task.annotations.hasUnsavedChanges()).toBe(true);
|
||||
await task.annotations.clear(true);
|
||||
annotations = await task.annotations.get(0);
|
||||
expect(annotations.length).not.toBe(0);
|
||||
expect(await task.annotations.hasUnsavedChanges()).toBe(false);
|
||||
});
|
||||
|
||||
test('clear annotations with reload in a job', async () => {
|
||||
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
|
||||
let annotations = await job.annotations.get(0);
|
||||
expect(annotations.length).not.toBe(0);
|
||||
annotations[0].occluded = true;
|
||||
await annotations[0].save();
|
||||
expect(await job.annotations.hasUnsavedChanges()).toBe(true);
|
||||
await job.annotations.clear(true);
|
||||
annotations = await job.annotations.get(0);
|
||||
expect(annotations.length).not.toBe(0);
|
||||
expect(await job.annotations.hasUnsavedChanges()).toBe(false);
|
||||
});
|
||||
|
||||
test('clear annotations with bad reload parameter', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
await task.annotations.clear(true);
|
||||
expect(task.annotations.clear('reload'))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Feature: get statistics', () => {
|
||||
test('get statistics from a task', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
await task.annotations.clear(true);
|
||||
const statistics = await task.annotations.statistics();
|
||||
expect(statistics).toBeInstanceOf(window.cvat.classes.Statistics);
|
||||
expect(statistics.total.total).toBe(29);
|
||||
});
|
||||
|
||||
test('get statistics from a job', async () => {
|
||||
const job = (await window.cvat.jobs.get({ jobID: 101 }))[0];
|
||||
await job.annotations.clear(true);
|
||||
const statistics = await job.annotations.statistics();
|
||||
expect(statistics).toBeInstanceOf(window.cvat.classes.Statistics);
|
||||
expect(statistics.total.total).toBe(512);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Feature: select object', () => {
|
||||
test('select object in a task', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
const annotations = await task.annotations.get(0);
|
||||
let result = await task.annotations.select(annotations, 1430, 765);
|
||||
expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.RECTANGLE);
|
||||
result = await task.annotations.select(annotations, 1415, 765);
|
||||
expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.POLYGON);
|
||||
expect(result.state.points.length).toBe(10);
|
||||
result = await task.annotations.select(annotations, 1083, 543);
|
||||
expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.POINTS);
|
||||
expect(result.state.points.length).toBe(16);
|
||||
result = await task.annotations.select(annotations, 613, 811);
|
||||
expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.POLYGON);
|
||||
expect(result.state.points.length).toBe(94);
|
||||
});
|
||||
|
||||
test('select object in a job', async () => {
|
||||
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
|
||||
const annotations = await job.annotations.get(0);
|
||||
let result = await job.annotations.select(annotations, 490, 540);
|
||||
expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.RECTANGLE);
|
||||
result = await job.annotations.select(annotations, 430, 260);
|
||||
expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.POLYLINE);
|
||||
result = await job.annotations.select(annotations, 1473, 250);
|
||||
expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.RECTANGLE);
|
||||
result = await job.annotations.select(annotations, 1490, 237);
|
||||
expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.POLYGON);
|
||||
expect(result.state.points.length).toBe(94);
|
||||
});
|
||||
|
||||
test('trying to select from not object states', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
const annotations = await task.annotations.get(0);
|
||||
expect(task.annotations.select(annotations.concat({}), 500, 500))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('trying to select with invalid coordinates', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
const annotations = await task.annotations.get(0);
|
||||
expect(task.annotations.select(annotations, null, null))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
expect(task.annotations.select(annotations, null, null))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
expect(task.annotations.select(annotations, '5', '10'))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:false
|
||||
jest:false
|
||||
describe:false
|
||||
*/
|
||||
|
||||
// Setup mock for a server
|
||||
jest.mock('../../src/server-proxy', () => {
|
||||
const mock = require('../mocks/server-proxy.mock');
|
||||
return mock;
|
||||
});
|
||||
|
||||
// Initialize api
|
||||
window.cvat = require('../../src/api');
|
||||
|
||||
const { FrameData } = require('../../src/frames');
|
||||
|
||||
describe('Feature: get frame meta', () => {
|
||||
test('get meta for a task', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
const frame = await task.frames.get(0);
|
||||
expect(frame).toBeInstanceOf(FrameData);
|
||||
});
|
||||
|
||||
test('get meta for a job', async () => {
|
||||
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
|
||||
const frame = await job.frames.get(0);
|
||||
expect(frame).toBeInstanceOf(FrameData);
|
||||
});
|
||||
|
||||
test('pass frame number out of a task', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
expect(task.frames.get(100))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
expect(task.frames.get(-1))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('pass bad frame number', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
expect(task.frames.get('5'))
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('do not pass any frame number', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
expect(task.frames.get())
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Feature: get frame data', () => {
|
||||
test('get frame data for a task', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
const frame = await task.frames.get(0);
|
||||
const frameData = await frame.data();
|
||||
expect(typeof (frameData)).toBe('string');
|
||||
});
|
||||
|
||||
test('get frame data for a job', async () => {
|
||||
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
|
||||
const frame = await job.frames.get(0);
|
||||
const frameData = await frame.data();
|
||||
expect(typeof (frameData)).toBe('string');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:false
|
||||
jest:false
|
||||
describe:false
|
||||
*/
|
||||
|
||||
// Setup mock for a server
|
||||
jest.mock('../../src/server-proxy', () => {
|
||||
const mock = require('../mocks/server-proxy.mock');
|
||||
return mock;
|
||||
});
|
||||
|
||||
// Initialize api
|
||||
window.cvat = require('../../src/api');
|
||||
|
||||
const { Job } = require('../../src/session');
|
||||
|
||||
|
||||
// Test cases
|
||||
describe('Feature: get a list of jobs', () => {
|
||||
test('get jobs by a task id', async () => {
|
||||
const result = await window.cvat.jobs.get({
|
||||
taskID: 3,
|
||||
});
|
||||
expect(Array.isArray(result)).toBeTruthy();
|
||||
expect(result).toHaveLength(2);
|
||||
for (const el of result) {
|
||||
expect(el).toBeInstanceOf(Job);
|
||||
}
|
||||
|
||||
expect(result[0].task.id).toBe(3);
|
||||
expect(result[0].task).toBe(result[1].task);
|
||||
});
|
||||
|
||||
test('get jobs by an unknown task id', async () => {
|
||||
const result = await window.cvat.jobs.get({
|
||||
taskID: 50,
|
||||
});
|
||||
expect(Array.isArray(result)).toBeTruthy();
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
|
||||
test('get jobs by a job id', async () => {
|
||||
const result = await window.cvat.jobs.get({
|
||||
jobID: 1,
|
||||
});
|
||||
expect(Array.isArray(result)).toBeTruthy();
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe(1);
|
||||
});
|
||||
|
||||
test('get jobs by an unknown job id', async () => {
|
||||
const result = await window.cvat.jobs.get({
|
||||
taskID: 50,
|
||||
});
|
||||
expect(Array.isArray(result)).toBeTruthy();
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('get jobs by invalid filter with both taskID and jobID', async () => {
|
||||
expect(window.cvat.jobs.get({
|
||||
taskID: 1,
|
||||
jobID: 1,
|
||||
})).rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('get jobs by invalid job id', async () => {
|
||||
expect(window.cvat.jobs.get({
|
||||
jobID: '1',
|
||||
})).rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('get jobs by invalid task id', async () => {
|
||||
expect(window.cvat.jobs.get({
|
||||
taskID: '1',
|
||||
})).rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('get jobs by unknown filter', async () => {
|
||||
expect(window.cvat.jobs.get({
|
||||
unknown: 50,
|
||||
})).rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Feature: save job', () => {
|
||||
test('save status of a job', async () => {
|
||||
let result = await window.cvat.jobs.get({
|
||||
jobID: 1,
|
||||
});
|
||||
|
||||
result[0].status = 'validation';
|
||||
await result[0].save();
|
||||
|
||||
result = await window.cvat.jobs.get({
|
||||
jobID: 1,
|
||||
});
|
||||
expect(result[0].status).toBe('validation');
|
||||
});
|
||||
|
||||
test('save invalid status of a job', async () => {
|
||||
const result = await window.cvat.jobs.get({
|
||||
jobID: 1,
|
||||
});
|
||||
|
||||
await result[0].save();
|
||||
expect(() => {
|
||||
result[0].status = 'invalid';
|
||||
}).toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,347 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:false
|
||||
jest:false
|
||||
describe:false
|
||||
*/
|
||||
|
||||
// Setup mock for a server
|
||||
jest.mock('../../src/server-proxy', () => {
|
||||
const mock = require('../mocks/server-proxy.mock');
|
||||
return mock;
|
||||
});
|
||||
|
||||
// Initialize api
|
||||
window.cvat = require('../../src/api');
|
||||
|
||||
describe('Feature: set attributes for an object state', () => {
|
||||
test('set a valid value', () => {
|
||||
const state = new window.cvat.classes.ObjectState({
|
||||
objectType: window.cvat.enums.ObjectType.SHAPE,
|
||||
shapeType: window.cvat.enums.ObjectShape.RECTANGLE,
|
||||
frame: 5,
|
||||
});
|
||||
|
||||
const attributes = {
|
||||
5: 'man',
|
||||
6: 'glasses',
|
||||
};
|
||||
|
||||
state.attributes = attributes;
|
||||
expect(state.attributes).toEqual(attributes);
|
||||
});
|
||||
|
||||
test('trying to set a bad value', () => {
|
||||
const state = new window.cvat.classes.ObjectState({
|
||||
objectType: window.cvat.enums.ObjectType.SHAPE,
|
||||
shapeType: window.cvat.enums.ObjectShape.RECTANGLE,
|
||||
frame: 5,
|
||||
});
|
||||
|
||||
let attributes = 'bad attribute';
|
||||
expect(() => {
|
||||
state.attributes = attributes;
|
||||
}).toThrow(window.cvat.exceptions.ArgumentError);
|
||||
|
||||
attributes = 5;
|
||||
expect(() => {
|
||||
state.attributes = attributes;
|
||||
}).toThrow(window.cvat.exceptions.ArgumentError);
|
||||
|
||||
attributes = false;
|
||||
expect(() => {
|
||||
state.attributes = attributes;
|
||||
}).toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Feature: set points for an object state', () => {
|
||||
test('set a valid value', () => {
|
||||
const state = new window.cvat.classes.ObjectState({
|
||||
objectType: window.cvat.enums.ObjectType.SHAPE,
|
||||
shapeType: window.cvat.enums.ObjectShape.RECTANGLE,
|
||||
frame: 5,
|
||||
});
|
||||
|
||||
const points = [1, 2, 3, 4];
|
||||
state.points = points;
|
||||
expect(state.points).toEqual(points);
|
||||
});
|
||||
|
||||
test('trying to set a bad value', () => {
|
||||
const state = new window.cvat.classes.ObjectState({
|
||||
objectType: window.cvat.enums.ObjectType.SHAPE,
|
||||
shapeType: window.cvat.enums.ObjectShape.RECTANGLE,
|
||||
frame: 5,
|
||||
});
|
||||
|
||||
let points = 'bad points';
|
||||
expect(() => {
|
||||
state.points = points;
|
||||
}).toThrow(window.cvat.exceptions.ArgumentError);
|
||||
|
||||
points = 5;
|
||||
expect(() => {
|
||||
state.points = points;
|
||||
}).toThrow(window.cvat.exceptions.ArgumentError);
|
||||
|
||||
points = false;
|
||||
expect(() => {
|
||||
state.points = points;
|
||||
}).toThrow(window.cvat.exceptions.ArgumentError);
|
||||
|
||||
points = {};
|
||||
expect(() => {
|
||||
state.points = points;
|
||||
}).toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Feature: save object from its state', () => {
|
||||
test('save valid values for a shape', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
const annotations = await task.annotations.get(0);
|
||||
let state = annotations[0];
|
||||
expect(state.objectType).toBe(window.cvat.enums.ObjectType.SHAPE);
|
||||
expect(state.shapeType).toBe(window.cvat.enums.ObjectShape.RECTANGLE);
|
||||
state.points = [0, 0, 100, 100];
|
||||
state.occluded = true;
|
||||
[, state.label] = task.labels;
|
||||
state.lock = true;
|
||||
state = await state.save();
|
||||
expect(state).toBeInstanceOf(window.cvat.classes.ObjectState);
|
||||
expect(state.label.id).toBe(task.labels[1].id);
|
||||
expect(state.lock).toBe(true);
|
||||
expect(state.occluded).toBe(true);
|
||||
expect(state.points).toEqual([0, 0, 100, 100]);
|
||||
});
|
||||
|
||||
test('save valid values for a track', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
|
||||
const annotations = await task.annotations.get(10);
|
||||
let state = annotations[1];
|
||||
expect(state.objectType).toBe(window.cvat.enums.ObjectType.TRACK);
|
||||
expect(state.shapeType).toBe(window.cvat.enums.ObjectShape.RECTANGLE);
|
||||
|
||||
state.occluded = true;
|
||||
state.lock = true;
|
||||
state.points = [100, 200, 200, 400];
|
||||
state.attributes = {
|
||||
1: 'sitting',
|
||||
3: 'female',
|
||||
2: '10',
|
||||
4: 'true',
|
||||
};
|
||||
|
||||
state = await state.save();
|
||||
expect(state).toBeInstanceOf(window.cvat.classes.ObjectState);
|
||||
expect(state.lock).toBe(true);
|
||||
expect(state.occluded).toBe(true);
|
||||
expect(state.points).toEqual([100, 200, 200, 400]);
|
||||
expect(state.attributes[1]).toBe('sitting');
|
||||
expect(state.attributes[2]).toBe('10');
|
||||
expect(state.attributes[3]).toBe('female');
|
||||
expect(state.attributes[4]).toBe('true');
|
||||
|
||||
state.lock = false;
|
||||
[state.label] = task.labels;
|
||||
state = await state.save();
|
||||
expect(state.label.id).toBe(task.labels[0].id);
|
||||
|
||||
state.outside = true;
|
||||
state = await state.save();
|
||||
expect(state.lock).toBe(false);
|
||||
expect(state.outside).toBe(true);
|
||||
|
||||
state.keyframe = false;
|
||||
state = await state.save();
|
||||
expect(state.keyframe).toBe(false);
|
||||
});
|
||||
|
||||
test('save bad values for a shape', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
const annotations = await task.annotations.get(0);
|
||||
const state = annotations[0];
|
||||
|
||||
state.occluded = 'false';
|
||||
await expect(state.save())
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
|
||||
const oldPoints = state.points;
|
||||
state.occluded = false;
|
||||
state.points = ['100', '50', '100', {}];
|
||||
await expect(state.save())
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
|
||||
state.points = oldPoints;
|
||||
state.lock = 'true';
|
||||
await expect(state.save())
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
|
||||
const oldLabel = state.label;
|
||||
state.lock = false;
|
||||
state.label = 1;
|
||||
await expect(state.save())
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
|
||||
state.label = oldLabel;
|
||||
state.attributes = { 1: {}, 2: false, 3: () => {} };
|
||||
await expect(state.save())
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('save bad values for a track', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
|
||||
const annotations = await task.annotations.get(0);
|
||||
const state = annotations[0];
|
||||
|
||||
state.occluded = 'false';
|
||||
await expect(state.save())
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
|
||||
const oldPoints = state.points;
|
||||
state.occluded = false;
|
||||
state.points = ['100', '50', '100', {}];
|
||||
await expect(state.save())
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
|
||||
state.points = oldPoints;
|
||||
state.lock = 'true';
|
||||
await expect(state.save())
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
|
||||
const oldLabel = state.label;
|
||||
state.lock = false;
|
||||
state.label = 1;
|
||||
await expect(state.save())
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
|
||||
state.label = oldLabel;
|
||||
state.outside = 5;
|
||||
await expect(state.save())
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
|
||||
state.outside = false;
|
||||
state.keyframe = '10';
|
||||
await expect(state.save())
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
|
||||
state.keyframe = true;
|
||||
state.attributes = { 1: {}, 2: false, 3: () => {} };
|
||||
await expect(state.save())
|
||||
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('trying to change locked shape', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
|
||||
const annotations = await task.annotations.get(0);
|
||||
let state = annotations[0];
|
||||
|
||||
state.lock = true;
|
||||
state = await state.save();
|
||||
|
||||
const { points } = state;
|
||||
state.points = [0, 0, 500, 500];
|
||||
state = await state.save();
|
||||
expect(state.points).toEqual(points);
|
||||
});
|
||||
|
||||
test('trying to set too small area of a shape', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
|
||||
const annotations = await task.annotations.get(0);
|
||||
let state = annotations[0];
|
||||
|
||||
const { points } = state;
|
||||
state.points = [0, 0, 2, 2]; // area is 4
|
||||
state = await state.save();
|
||||
expect(state.points).toEqual(points);
|
||||
});
|
||||
|
||||
test('trying to set too small area of a track', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
|
||||
const annotations = await task.annotations.get(0);
|
||||
let state = annotations[0];
|
||||
|
||||
const { points } = state;
|
||||
state.points = [0, 0, 2, 2]; // area is 4
|
||||
state = await state.save();
|
||||
expect(state.points).toEqual(points);
|
||||
});
|
||||
|
||||
test('trying to set too small length of a shape', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
const annotations = await task.annotations.get(0);
|
||||
let state = annotations[8];
|
||||
|
||||
const { points } = state;
|
||||
state.points = [0, 0, 2, 2]; // length is 2
|
||||
state = await state.save();
|
||||
expect(state.points).toEqual(points);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Feature: delete object', () => {
|
||||
test('delete a shape', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
const annotationsBefore = await task.annotations.get(0);
|
||||
const { length } = annotationsBefore;
|
||||
await annotationsBefore[0].delete();
|
||||
const annotationsAfter = await task.annotations.get(0);
|
||||
expect(annotationsAfter).toHaveLength(length - 1);
|
||||
});
|
||||
|
||||
test('delete a track', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
|
||||
const annotationsBefore = await task.annotations.get(0);
|
||||
const { length } = annotationsBefore;
|
||||
await annotationsBefore[0].delete();
|
||||
const annotationsAfter = await task.annotations.get(0);
|
||||
expect(annotationsAfter).toHaveLength(length - 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Feature: change z order of an object', () => {
|
||||
test('up z order for a shape', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
const annotations = await task.annotations.get(0);
|
||||
const state = annotations[0];
|
||||
|
||||
const { zOrder } = state;
|
||||
await state.up();
|
||||
expect(state.zOrder).toBeGreaterThan(zOrder);
|
||||
});
|
||||
|
||||
test('up z order for a track', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
|
||||
const annotations = await task.annotations.get(0);
|
||||
const state = annotations[0];
|
||||
|
||||
const { zOrder } = state;
|
||||
await state.up();
|
||||
expect(state.zOrder).toBeGreaterThan(zOrder);
|
||||
});
|
||||
|
||||
test('down z order for a shape', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
|
||||
const annotations = await task.annotations.get(0);
|
||||
const state = annotations[0];
|
||||
|
||||
const { zOrder } = state;
|
||||
await state.down();
|
||||
expect(state.zOrder).toBeLessThan(zOrder);
|
||||
});
|
||||
|
||||
test('down z order for a track', async () => {
|
||||
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
|
||||
const annotations = await task.annotations.get(0);
|
||||
const state = annotations[0];
|
||||
|
||||
const { zOrder } = state;
|
||||
await state.down();
|
||||
expect(state.zOrder).toBeLessThan(zOrder);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:false
|
||||
jest:false
|
||||
describe:false
|
||||
*/
|
||||
|
||||
// Setup mock for a server
|
||||
jest.mock('../../src/server-proxy', () => {
|
||||
const mock = require('../mocks/server-proxy.mock');
|
||||
return mock;
|
||||
});
|
||||
|
||||
// Initialize api
|
||||
window.cvat = require('../../src/api');
|
||||
|
||||
describe('Feature: dummy feature', () => {
|
||||
test('dummy test', async () => {
|
||||
// TODO: Write test after design of plugin system
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
const plugin = {
|
||||
name: 'Example Plugin',
|
||||
description: 'This example plugin demonstrates how plugin system in CVAT works',
|
||||
cvat: {
|
||||
server: {
|
||||
about: {
|
||||
async leave(self, result) {
|
||||
result.plugins = await self.internal.getPlugins();
|
||||
return result;
|
||||
},
|
||||
},
|
||||
},
|
||||
classes: {
|
||||
Job: {
|
||||
prototype: {
|
||||
annotations: {
|
||||
put: {
|
||||
enter(self, objects) {
|
||||
for (const obj of objects) {
|
||||
if (obj.type !== 'tag') {
|
||||
const points = obj.position.map((point) => {
|
||||
const roundPoint = {
|
||||
x: Math.round(point.x),
|
||||
y: Math.round(point.y),
|
||||
};
|
||||
return roundPoint;
|
||||
});
|
||||
obj.points = points;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
internal: {
|
||||
async getPlugins() {
|
||||
const plugins = await window.cvat.plugins.list();
|
||||
return plugins.map((el) => {
|
||||
const obj = {
|
||||
name: el.name,
|
||||
description: el.description,
|
||||
};
|
||||
return obj;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
async function test() {
|
||||
await window.cvat.plugins.register(plugin);
|
||||
await window.cvat.server.login('admin', 'nimda760');
|
||||
|
||||
try {
|
||||
console.log(JSON.stringify(await window.cvat.server.about()));
|
||||
console.log(await window.cvat.users.get({ self: false }));
|
||||
console.log(await window.cvat.users.get({ self: true }));
|
||||
console.log(JSON.stringify(await window.cvat.jobs.get({ taskID: 8 })));
|
||||
console.log(JSON.stringify(await window.cvat.jobs.get({ jobID: 10 })));
|
||||
console.log(await window.cvat.tasks.get());
|
||||
console.log(await window.cvat.tasks.get({ id: 8 }));
|
||||
console.log('Done.');
|
||||
} catch (exception) {
|
||||
console.log(exception.constructor.name);
|
||||
console.log(exception.message);
|
||||
}
|
||||
}
|
||||
*/
|
||||
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:false
|
||||
jest:false
|
||||
describe:false
|
||||
*/
|
||||
|
||||
// Setup mock for a server
|
||||
jest.mock('../../src/server-proxy', () => {
|
||||
const mock = require('../mocks/server-proxy.mock');
|
||||
return mock;
|
||||
});
|
||||
|
||||
// Initialize api
|
||||
window.cvat = require('../../src/api');
|
||||
const {
|
||||
AnnotationFormat,
|
||||
Loader,
|
||||
Dumper,
|
||||
} = require('../../src/annotation-format');
|
||||
|
||||
// Test cases
|
||||
describe('Feature: get info about cvat', () => {
|
||||
test('get info about server', async () => {
|
||||
const result = await window.cvat.server.about();
|
||||
expect(result).toBeInstanceOf(Object);
|
||||
expect('name' in result).toBeTruthy();
|
||||
expect('description' in result).toBeTruthy();
|
||||
expect('version' in result).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Feature: get share storage info', () => {
|
||||
test('get files in a root of a share storage', async () => {
|
||||
const result = await window.cvat.server.share();
|
||||
expect(Array.isArray(result)).toBeTruthy();
|
||||
expect(result).toHaveLength(5);
|
||||
});
|
||||
|
||||
test('get files in a some dir of a share storage', async () => {
|
||||
const result = await window.cvat.server.share('images');
|
||||
expect(Array.isArray(result)).toBeTruthy();
|
||||
expect(result).toHaveLength(8);
|
||||
});
|
||||
|
||||
test('get files in a some unknown dir of a share storage', async () => {
|
||||
expect(window.cvat.server.share(
|
||||
'Unknown Directory',
|
||||
)).rejects.toThrow(window.cvat.exceptions.ServerError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Feature: get annotation formats', () => {
|
||||
test('get annotation formats from a server', async () => {
|
||||
const result = await window.cvat.server.formats();
|
||||
expect(Array.isArray(result)).toBeTruthy();
|
||||
for (const format of result) {
|
||||
expect(format).toBeInstanceOf(AnnotationFormat);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Feature: get annotation loaders', () => {
|
||||
test('get annotation formats from a server', async () => {
|
||||
const result = await window.cvat.server.formats();
|
||||
expect(Array.isArray(result)).toBeTruthy();
|
||||
for (const format of result) {
|
||||
expect(format).toBeInstanceOf(AnnotationFormat);
|
||||
const { loaders } = format;
|
||||
expect(Array.isArray(loaders)).toBeTruthy();
|
||||
for (const loader of loaders) {
|
||||
expect(loader).toBeInstanceOf(Loader);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Feature: get annotation dumpers', () => {
|
||||
test('get annotation formats from a server', async () => {
|
||||
const result = await window.cvat.server.formats();
|
||||
expect(Array.isArray(result)).toBeTruthy();
|
||||
for (const format of result) {
|
||||
expect(format).toBeInstanceOf(AnnotationFormat);
|
||||
const { dumpers } = format;
|
||||
expect(Array.isArray(dumpers)).toBeTruthy();
|
||||
for (const dumper of dumpers) {
|
||||
expect(dumper).toBeInstanceOf(Dumper);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:false
|
||||
jest:false
|
||||
describe:false
|
||||
*/
|
||||
|
||||
// Setup mock for a server
|
||||
jest.mock('../../src/server-proxy', () => {
|
||||
const mock = require('../mocks/server-proxy.mock');
|
||||
return mock;
|
||||
});
|
||||
|
||||
// Initialize api
|
||||
window.cvat = require('../../src/api');
|
||||
|
||||
const { Task } = require('../../src/session');
|
||||
|
||||
|
||||
// Test cases
|
||||
describe('Feature: get a list of tasks', () => {
|
||||
test('get all tasks', async () => {
|
||||
const result = await window.cvat.tasks.get();
|
||||
expect(Array.isArray(result)).toBeTruthy();
|
||||
expect(result).toHaveLength(6);
|
||||
for (const el of result) {
|
||||
expect(el).toBeInstanceOf(Task);
|
||||
}
|
||||
});
|
||||
|
||||
test('get a task by an id', async () => {
|
||||
const result = await window.cvat.tasks.get({
|
||||
id: 3,
|
||||
});
|
||||
expect(Array.isArray(result)).toBeTruthy();
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toBeInstanceOf(Task);
|
||||
expect(result[0].id).toBe(3);
|
||||
});
|
||||
|
||||
test('get a task by an unknown id', async () => {
|
||||
const result = await window.cvat.tasks.get({
|
||||
id: 50,
|
||||
});
|
||||
expect(Array.isArray(result)).toBeTruthy();
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('get a task by an invalid id', async () => {
|
||||
expect(window.cvat.tasks.get({
|
||||
id: '50',
|
||||
})).rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('get tasks by filters', async () => {
|
||||
const result = await window.cvat.tasks.get({
|
||||
mode: 'interpolation',
|
||||
});
|
||||
expect(Array.isArray(result)).toBeTruthy();
|
||||
expect(result).toHaveLength(3);
|
||||
for (const el of result) {
|
||||
expect(el).toBeInstanceOf(Task);
|
||||
expect(el.mode).toBe('interpolation');
|
||||
}
|
||||
});
|
||||
|
||||
test('get tasks by invalid filters', async () => {
|
||||
expect(window.cvat.tasks.get({
|
||||
unknown: '5',
|
||||
})).rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('get task by name, status and mode', async () => {
|
||||
const result = await window.cvat.tasks.get({
|
||||
mode: 'interpolation',
|
||||
status: 'annotation',
|
||||
name: 'Test Task',
|
||||
});
|
||||
expect(Array.isArray(result)).toBeTruthy();
|
||||
expect(result).toHaveLength(1);
|
||||
for (const el of result) {
|
||||
expect(el).toBeInstanceOf(Task);
|
||||
expect(el.mode).toBe('interpolation');
|
||||
expect(el.status).toBe('annotation');
|
||||
expect(el.name).toBe('Test Task');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Feature: save a task', () => {
|
||||
test('save some changed fields in a task', async () => {
|
||||
let result = await window.cvat.tasks.get({
|
||||
id: 2,
|
||||
});
|
||||
|
||||
result[0].bugTracker = 'newBugTracker';
|
||||
result[0].zOrder = true;
|
||||
result[0].name = 'New Task Name';
|
||||
|
||||
result[0].save();
|
||||
|
||||
result = await window.cvat.tasks.get({
|
||||
id: 2,
|
||||
});
|
||||
|
||||
expect(result[0].bugTracker).toBe('newBugTracker');
|
||||
expect(result[0].zOrder).toBe(true);
|
||||
expect(result[0].name).toBe('New Task Name');
|
||||
});
|
||||
|
||||
test('save some new labels in a task', async () => {
|
||||
let result = await window.cvat.tasks.get({
|
||||
id: 2,
|
||||
});
|
||||
|
||||
const labelsLength = result[0].labels.length;
|
||||
const newLabel = new window.cvat.classes.Label({
|
||||
name: 'My boss\'s car',
|
||||
attributes: [{
|
||||
default_value: 'false',
|
||||
input_type: 'checkbox',
|
||||
mutable: true,
|
||||
name: 'parked',
|
||||
values: ['false'],
|
||||
}],
|
||||
});
|
||||
|
||||
result[0].labels = [newLabel];
|
||||
result[0].save();
|
||||
|
||||
result = await window.cvat.tasks.get({
|
||||
id: 2,
|
||||
});
|
||||
|
||||
expect(result[0].labels).toHaveLength(labelsLength + 1);
|
||||
const appendedLabel = result[0].labels.filter((el) => el.name === 'My boss\'s car');
|
||||
expect(appendedLabel).toHaveLength(1);
|
||||
expect(appendedLabel[0].attributes).toHaveLength(1);
|
||||
expect(appendedLabel[0].attributes[0].name).toBe('parked');
|
||||
expect(appendedLabel[0].attributes[0].defaultValue).toBe('false');
|
||||
expect(appendedLabel[0].attributes[0].mutable).toBe(true);
|
||||
expect(appendedLabel[0].attributes[0].inputType).toBe('checkbox');
|
||||
});
|
||||
|
||||
test('save new task without an id', async () => {
|
||||
const task = new window.cvat.classes.Task({
|
||||
name: 'New Task',
|
||||
labels: [{
|
||||
name: 'My boss\'s car',
|
||||
attributes: [{
|
||||
default_value: 'false',
|
||||
input_type: 'checkbox',
|
||||
mutable: true,
|
||||
name: 'parked',
|
||||
values: ['false'],
|
||||
}],
|
||||
}],
|
||||
bug_tracker: 'bug tracker value',
|
||||
image_quality: 50,
|
||||
z_order: true,
|
||||
});
|
||||
|
||||
const result = await task.save();
|
||||
expect(typeof (result.id)).toBe('number');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Feature: delete a task', () => {
|
||||
test('delete a task', async () => {
|
||||
let result = await window.cvat.tasks.get({
|
||||
id: 3,
|
||||
});
|
||||
|
||||
await result[0].delete();
|
||||
result = await window.cvat.tasks.get({
|
||||
id: 3,
|
||||
});
|
||||
|
||||
expect(Array.isArray(result)).toBeTruthy();
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:false
|
||||
jest:false
|
||||
describe:false
|
||||
*/
|
||||
|
||||
// Setup mock for a server
|
||||
jest.mock('../../src/server-proxy', () => {
|
||||
const mock = require('../mocks/server-proxy.mock');
|
||||
return mock;
|
||||
});
|
||||
|
||||
// Initialize api
|
||||
window.cvat = require('../../src/api');
|
||||
|
||||
const User = require('../../src/user');
|
||||
|
||||
// Test cases
|
||||
describe('Feature: get a list of users', () => {
|
||||
test('get all users', async () => {
|
||||
const result = await window.cvat.users.get();
|
||||
expect(Array.isArray(result)).toBeTruthy();
|
||||
expect(result).toHaveLength(2);
|
||||
for (const el of result) {
|
||||
expect(el).toBeInstanceOf(User);
|
||||
}
|
||||
});
|
||||
|
||||
test('get only self', async () => {
|
||||
const result = await window.cvat.users.get({
|
||||
self: true,
|
||||
});
|
||||
expect(Array.isArray(result)).toBeTruthy();
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toBeInstanceOf(User);
|
||||
});
|
||||
|
||||
test('get users with unknown filter key', async () => {
|
||||
expect(window.cvat.users.get({
|
||||
unknown: '50',
|
||||
})).rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('get users with invalid filter key', async () => {
|
||||
expect(window.cvat.users.get({
|
||||
self: 1,
|
||||
})).rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,302 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* eslint import/no-extraneous-dependencies: 0 */
|
||||
|
||||
/* global
|
||||
require:false
|
||||
*/
|
||||
|
||||
const {
|
||||
tasksDummyData,
|
||||
aboutDummyData,
|
||||
formatsDummyData,
|
||||
shareDummyData,
|
||||
usersDummyData,
|
||||
taskAnnotationsDummyData,
|
||||
jobAnnotationsDummyData,
|
||||
frameMetaDummyData,
|
||||
} = require('./dummy-data.mock');
|
||||
|
||||
class ServerProxy {
|
||||
constructor() {
|
||||
async function about() {
|
||||
return JSON.parse(JSON.stringify(aboutDummyData));
|
||||
}
|
||||
|
||||
async function share(directory) {
|
||||
let position = shareDummyData;
|
||||
|
||||
// Emulation of internal directories
|
||||
if (directory.length > 1) {
|
||||
const components = directory.split('/');
|
||||
|
||||
for (const component of components) {
|
||||
const idx = position.map(x => x.name).indexOf(component);
|
||||
if (idx !== -1 && 'children' in position[idx]) {
|
||||
position = position[idx].children;
|
||||
} else {
|
||||
throw new window.cvat.exceptions.ServerError(
|
||||
`${component} is not a valid directory`,
|
||||
400,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.parse(JSON.stringify(position));
|
||||
}
|
||||
|
||||
async function formats() {
|
||||
return JSON.parse(JSON.stringify(formatsDummyData));
|
||||
}
|
||||
|
||||
async function exception() {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function login() {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function getTasks(filter = '') {
|
||||
function QueryStringToJSON(query) {
|
||||
const pairs = [...new URLSearchParams(query).entries()];
|
||||
|
||||
const result = {};
|
||||
for (const pair of pairs) {
|
||||
const [key, value] = pair;
|
||||
if (['id'].includes(key)) {
|
||||
result[key] = +value;
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.parse(JSON.stringify(result));
|
||||
}
|
||||
|
||||
// Emulation of a query filter
|
||||
const queries = QueryStringToJSON(filter);
|
||||
const result = tasksDummyData.results.filter((x) => {
|
||||
for (const key in queries) {
|
||||
if (Object.prototype.hasOwnProperty.call(queries, key)) {
|
||||
// TODO: Particular match for some fields is not checked
|
||||
if (queries[key] !== x[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function saveTask(id, taskData) {
|
||||
const object = tasksDummyData.results.filter(task => task.id === id)[0];
|
||||
for (const prop in taskData) {
|
||||
if (Object.prototype.hasOwnProperty.call(taskData, prop)
|
||||
&& Object.prototype.hasOwnProperty.call(object, prop)) {
|
||||
object[prop] = taskData[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createTask(taskData) {
|
||||
const id = Math.max(...tasksDummyData.results.map(el => el.id)) + 1;
|
||||
tasksDummyData.results.push({
|
||||
id,
|
||||
url: `http://localhost:7000/api/v1/tasks/${id}`,
|
||||
name: taskData.name,
|
||||
size: 5000,
|
||||
mode: 'interpolation',
|
||||
owner: 2,
|
||||
assignee: null,
|
||||
bug_tracker: taskData.bug_tracker,
|
||||
created_date: '2019-05-16T13:08:00.621747+03:00',
|
||||
updated_date: '2019-05-16T13:08:00.621797+03:00',
|
||||
overlap: taskData.overlap ? taskData.overlap : 5,
|
||||
segment_size: taskData.segment_size ? taskData.segment_size : 5000,
|
||||
z_order: taskData.z_order,
|
||||
flipped: false,
|
||||
status: 'annotation',
|
||||
image_quality: taskData.image_quality,
|
||||
labels: JSON.parse(JSON.stringify(taskData.labels)),
|
||||
});
|
||||
|
||||
const createdTask = await getTasks(`?id=${id}`);
|
||||
return createdTask[0];
|
||||
}
|
||||
|
||||
async function deleteTask(id) {
|
||||
const tasks = tasksDummyData.results;
|
||||
const task = tasks.filter(el => el.id === id)[0];
|
||||
if (task) {
|
||||
tasks.splice(tasks.indexOf(task), 1);
|
||||
}
|
||||
}
|
||||
|
||||
async function getJob(jobID) {
|
||||
const jobs = tasksDummyData.results.reduce((acc, task) => {
|
||||
for (const segment of task.segments) {
|
||||
for (const job of segment.jobs) {
|
||||
const copy = JSON.parse(JSON.stringify(job));
|
||||
copy.start_frame = segment.start_frame;
|
||||
copy.stop_frame = segment.stop_frame;
|
||||
copy.task_id = task.id;
|
||||
|
||||
acc.push(copy);
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []).filter(job => job.id === jobID);
|
||||
|
||||
return jobs[0] || {
|
||||
detail: 'Not found.',
|
||||
};
|
||||
}
|
||||
|
||||
async function saveJob(id, jobData) {
|
||||
const object = tasksDummyData.results.reduce((acc, task) => {
|
||||
for (const segment of task.segments) {
|
||||
for (const job of segment.jobs) {
|
||||
acc.push(job);
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []).filter(job => job.id === id)[0];
|
||||
|
||||
for (const prop in jobData) {
|
||||
if (Object.prototype.hasOwnProperty.call(jobData, prop)
|
||||
&& Object.prototype.hasOwnProperty.call(object, prop)) {
|
||||
object[prop] = jobData[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getUsers() {
|
||||
return JSON.parse(JSON.stringify(usersDummyData)).results;
|
||||
}
|
||||
|
||||
async function getSelf() {
|
||||
return JSON.parse(JSON.stringify(usersDummyData)).results[0];
|
||||
}
|
||||
|
||||
async function getData() {
|
||||
return 'DUMMY_IMAGE';
|
||||
}
|
||||
|
||||
async function getMeta(tid) {
|
||||
return JSON.parse(JSON.stringify(frameMetaDummyData[tid]));
|
||||
}
|
||||
|
||||
async function getAnnotations(session, id) {
|
||||
if (session === 'task') {
|
||||
return JSON.parse(JSON.stringify(taskAnnotationsDummyData[id]));
|
||||
}
|
||||
|
||||
if (session === 'job') {
|
||||
return JSON.parse(JSON.stringify(jobAnnotationsDummyData[id]));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function updateAnnotations(session, id, data, action) {
|
||||
// Actually we do not change our dummy data
|
||||
// We just update the argument in some way and return it
|
||||
|
||||
data.version += 1;
|
||||
|
||||
if (action === 'create') {
|
||||
let idGenerator = 1000;
|
||||
data.tracks.concat(data.tags).concat(data.shapes).map((el) => {
|
||||
el.id = ++idGenerator;
|
||||
return el;
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
if (action === 'update') {
|
||||
return data;
|
||||
}
|
||||
|
||||
if (action === 'delete') {
|
||||
return data;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Object.defineProperties(this, Object.freeze({
|
||||
server: {
|
||||
value: Object.freeze({
|
||||
about,
|
||||
share,
|
||||
formats,
|
||||
exception,
|
||||
login,
|
||||
logout,
|
||||
}),
|
||||
writable: false,
|
||||
},
|
||||
|
||||
tasks: {
|
||||
value: Object.freeze({
|
||||
getTasks,
|
||||
saveTask,
|
||||
createTask,
|
||||
deleteTask,
|
||||
}),
|
||||
writable: false,
|
||||
},
|
||||
|
||||
jobs: {
|
||||
value: Object.freeze({
|
||||
getJob,
|
||||
saveJob,
|
||||
}),
|
||||
writable: false,
|
||||
},
|
||||
|
||||
users: {
|
||||
value: Object.freeze({
|
||||
getUsers,
|
||||
getSelf,
|
||||
}),
|
||||
writable: false,
|
||||
},
|
||||
|
||||
frames: {
|
||||
value: Object.freeze({
|
||||
getData,
|
||||
getMeta,
|
||||
}),
|
||||
writable: false,
|
||||
},
|
||||
|
||||
annotations: {
|
||||
value: {
|
||||
updateAnnotations,
|
||||
getAnnotations,
|
||||
},
|
||||
// To implement on of important tests
|
||||
writable: true,
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
const serverProxy = new ServerProxy();
|
||||
module.exports = serverProxy;
|
||||
@ -0,0 +1,72 @@
|
||||
/* global
|
||||
require:true,
|
||||
__dirname:true,
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
|
||||
const nodeConfig = {
|
||||
target: 'node',
|
||||
mode: 'production',
|
||||
devtool: 'source-map',
|
||||
entry: './src/api.js',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'cvat-core.node.js',
|
||||
library: 'cvat',
|
||||
libraryTarget: 'commonjs',
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
test: /.js?$/,
|
||||
exclude: /node_modules/,
|
||||
}],
|
||||
},
|
||||
externals: {
|
||||
canvas: 'commonjs canvas',
|
||||
},
|
||||
stats: {
|
||||
warnings: false,
|
||||
},
|
||||
};
|
||||
|
||||
const webConfig = {
|
||||
target: 'web',
|
||||
mode: 'production',
|
||||
devtool: 'source-map',
|
||||
entry: './src/api.js',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'cvat-core.min.js',
|
||||
library: 'cvat',
|
||||
libraryTarget: 'window',
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
test: /.js?$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
['@babel/preset-env', {
|
||||
targets: {
|
||||
chrome: 58,
|
||||
},
|
||||
useBuiltIns: 'usage',
|
||||
corejs: 3,
|
||||
loose: false,
|
||||
spec: false,
|
||||
debug: false,
|
||||
include: [],
|
||||
exclude: [],
|
||||
}],
|
||||
],
|
||||
sourceType: 'unambiguous',
|
||||
},
|
||||
},
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = [nodeConfig, webConfig];
|
||||
@ -0,0 +1,2 @@
|
||||
build
|
||||
node_modules
|
||||
@ -0,0 +1,9 @@
|
||||
REACT_APP_VERSION=${npm_package_version}
|
||||
|
||||
REACT_APP_API_PROTOCOL=http
|
||||
REACT_APP_API_HOST=localhost
|
||||
REACT_APP_API_PORT=7000
|
||||
REACT_APP_API_HOST_URL=${REACT_APP_API_PROTOCOL}://${REACT_APP_API_HOST}:${REACT_APP_API_PORT}
|
||||
REACT_APP_API_FULL_URL=${REACT_APP_API_PROTOCOL}://${REACT_APP_API_HOST}:${REACT_APP_API_PORT}/api/v1
|
||||
|
||||
SKIP_PREFLIGHT_CHECK=true
|
||||
@ -0,0 +1,9 @@
|
||||
REACT_APP_VERSION=${npm_package_version}
|
||||
|
||||
REACT_APP_API_PROTOCOL=http
|
||||
REACT_APP_API_HOST=localhost
|
||||
REACT_APP_API_PORT=8080
|
||||
REACT_APP_API_HOST_URL=${REACT_APP_API_PROTOCOL}://${REACT_APP_API_HOST}:${REACT_APP_API_PORT}
|
||||
REACT_APP_API_FULL_URL=${REACT_APP_API_PROTOCOL}://${REACT_APP_API_HOST}:${REACT_APP_API_PORT}/api/v1
|
||||
|
||||
SKIP_PREFLIGHT_CHECK=true
|
||||
@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
@ -0,0 +1,36 @@
|
||||
FROM ubuntu:18.04 AS cvat-ui
|
||||
|
||||
ARG http_proxy
|
||||
ARG https_proxy
|
||||
ARG no_proxy
|
||||
ARG socks_proxy
|
||||
|
||||
ENV TERM=xterm \
|
||||
http_proxy=${http_proxy} \
|
||||
https_proxy=${https_proxy} \
|
||||
no_proxy=${no_proxy} \
|
||||
socks_proxy=${socks_proxy}
|
||||
|
||||
ENV LANG='C.UTF-8' \
|
||||
LC_ALL='C.UTF-8'
|
||||
|
||||
# Install necessary apt packages
|
||||
RUN apt update && apt install -yq nodejs npm curl && \
|
||||
npm install -g n && n 10.16.3
|
||||
|
||||
# Create output directory
|
||||
RUN mkdir /tmp/cvat-ui
|
||||
WORKDIR /tmp/cvat-ui/
|
||||
|
||||
# Install dependencies
|
||||
COPY package*.json /tmp/cvat-ui/
|
||||
RUN npm install
|
||||
|
||||
# Build source code
|
||||
COPY . /tmp/cvat-ui/
|
||||
RUN mv .env.production .env && npm run build
|
||||
|
||||
FROM nginx
|
||||
# Replace default.conf configuration to remove unnecessary rules
|
||||
COPY react_nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=cvat-ui /tmp/cvat-ui/build /usr/share/nginx/html/
|
||||
@ -0,0 +1,14 @@
|
||||
const { override, fixBabelImports, addLessLoader } = require('customize-cra');
|
||||
|
||||
module.exports = override(
|
||||
fixBabelImports('import', {
|
||||
libraryName: 'antd',
|
||||
libraryDirectory: 'es',
|
||||
style: true,
|
||||
}),
|
||||
|
||||
addLessLoader({
|
||||
javascriptEnabled: true,
|
||||
// modifyVars: { '@primary-color': '#1DA57A' },
|
||||
}),
|
||||
);
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "cvat-ui",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@types/jest": "24.0.13",
|
||||
"@types/node": "^12.0.3",
|
||||
"@types/react": "16.8.19",
|
||||
"@types/react-dom": "16.8.4",
|
||||
"@types/react-redux": "^7.1.1",
|
||||
"@types/react-router-dom": "^4.3.4",
|
||||
"@types/redux-logger": "^3.0.7",
|
||||
"antd": "^3.19.1",
|
||||
"babel-plugin-import": "^1.11.2",
|
||||
"customize-cra": "^0.2.12",
|
||||
"less": "^3.9.0",
|
||||
"less-loader": "^5.0.0",
|
||||
"node-sass": "^4.12.0",
|
||||
"query-string": "^6.8.1",
|
||||
"react": "^16.8.6",
|
||||
"react-app-rewired": "^2.1.3",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-redux": "^7.1.0",
|
||||
"react-router-dom": "^5.0.1",
|
||||
"react-scripts": "3.0.1",
|
||||
"react-scripts-ts": "^3.1.0",
|
||||
"redux": "^4.0.4",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"source-map-explorer": "^1.8.0",
|
||||
"typescript": "3.4.5"
|
||||
},
|
||||
"scripts": {
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||
"start": "react-app-rewired start",
|
||||
"build": "react-app-rewired build",
|
||||
"test": "react-app-rewired test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
@ -0,0 +1 @@
|
||||
<svg width="98" height="27" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path d="M101 0v29l-52.544.001C44.326 35.511 35.598 40 25.5 40 11.417 40 0 31.27 0 20.5S11.417 1 25.5 1c4.542 0 8.807.908 12.5 2.5V0h63z" id="a"/></defs><g transform="translate(-2 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><path d="M48.142 1c4.736 0 6.879 3.234 6.879 5.904v2.068h-4.737V6.904c0-.79-.789-2.144-2.142-2.144-1.654 0-2.368 1.354-2.368 2.144v15.192c0 .79.714 2.144 2.368 2.144 1.353 0 2.142-1.354 2.142-2.144v-2.068h4.737v2.068c0 2.67-2.143 5.904-6.88 5.904C42.956 28 41 24.766 41 22.134V6.904C41 4.234 42.955 1 48.142 1zM19-6c9.389 0 17 7.611 17 17s-7.611 17-17 17S2 20.389 2 11 9.611-6 19-6zm42.256 7.338l3.345 19.48h.075l3.42-19.48h5l-6.052 26.324h-5L56.22 1.338h5.037zm20.706 0l5.413 26.324h-4.699l-.94-6.13h-4.548l-.902 6.13h-4.435l5.413-26.324h4.698zm18.038 0v3.723h-4.849v22.6h-4.699v-22.6h-4.81V1.338H100zM19 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14zm60.557 4.295h-.113l-1.466 9.439h3.007l-1.428-9.439z" fill="#000" fill-rule="nonzero" mask="url(#b)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1 @@
|
||||
<svg width="90" height="78" xmlns="http://www.w3.org/2000/svg"><path d="M84.27 0c2.753 0 5.007 2.167 5.12 4.874l.005.215v67.148c0 2.734-2.183 4.972-4.908 5.085l-.217.004H5.123c-2.751 0-5.005-2.167-5.118-4.874L0 72.237V5.09C0 2.355 2.183.117 4.907.004L5.123 0H84.27zm1.58 16.242H3.546v55.995c0 .816.632 1.488 1.434 1.56l.144.007H84.27c.824 0 1.501-.627 1.574-1.424l.007-.143V16.242zM12.658 38.48h4.328c.59 0 1.076.452 1.138 1.031l.007.126v1.03h15.02v-1.03c0-.596.446-1.087 1.02-1.15l.125-.007h4.328c.59 0 1.076.451 1.138 1.03l.006.127v4.372c0 .596-.446 1.087-1.02 1.15l-.124.007h-1.02v10.548h1.019c.59 0 1.077.451 1.139 1.031l.006.126v4.372c0 .596-.446 1.087-1.02 1.15l-.125.007h-4.327a1.15 1.15 0 0 1-1.139-1.03l-.006-.127v-1.03H18.13v1.03c0 .596-.446 1.087-1.02 1.15l-.125.007h-4.328a1.15 1.15 0 0 1-1.138-1.03l-.007-.127v-4.372c0-.596.447-1.087 1.02-1.15l.125-.007h1.019V45.166h-1.02a1.15 1.15 0 0 1-1.138-1.03l-.006-.127v-4.372c0-.596.446-1.087 1.02-1.15l.125-.007h4.328zm3.183 19.548h-2.038v2.058h2.038v-2.058zm21.638 0h-2.039v2.058h2.039v-2.058zM33.15 42.98H18.13v1.03c0 .595-.447 1.087-1.02 1.15l-.125.006h-1.019v10.548h1.019c.59 0 1.076.451 1.138 1.031l.007.126V57.9h15.02V56.87c0-.596.446-1.087 1.02-1.15l.125-.007h1.019V45.166h-1.019a1.15 1.15 0 0 1-1.139-1.031l-.006-.126v-1.03zm21.575-7.62c.398 0 .72.338.72.755v.67h9.458v-.67c0-.417.323-.755.721-.755h2.725c.398 0 .72.338.72.754v2.852c0 .416-.322.754-.72.754h-.641v6.88h.64c.399 0 .722.337.722.754v2.852c0 .416-.323.754-.721.754h-2.725c-.398 0-.721-.338-.721-.754v-.672h-9.457v.672c0 .416-.322.754-.72.754H52c-.398 0-.72-.338-.72-.754v-2.852c0-.416.322-.754.72-.754h.642v-6.88H52c-.398 0-.72-.337-.72-.754v-2.851c0-.417.322-.755.72-.755zm-.72 12.749H52.72v1.342h1.283V48.11zm13.623 0h-1.283v1.342h1.283V48.11zm-2.725-9.814h-9.457v.671c0 .417-.323.755-.721.755h-.641V46.6h.641c.399 0 .721.337.721.754v.671h9.457v-.67c0-.417.323-.755.72-.755h.642v-6.88h-.64c-.4 0-.722-.338-.722-.754v-.671zm-49.063 2.5h-2.038v2.058h2.038v-2.058zm21.638-.001H35.44v2.058h2.038v-2.058zm30.15-3.925h-1.283v1.342h1.283V36.87zm-13.624 0h-1.283v1.343h1.283v-1.343zM9.098 19.76c.93 0 1.692.711 1.766 1.616l.006.145v.118c0 .973-.793 1.761-1.772 1.761-.93 0-1.693-.711-1.767-1.616l-.005-.145v-.118c0-.973.793-1.761 1.772-1.761zm21.65 0c.978 0 1.771.788 1.771 1.76 0 .974-.793 1.762-1.771 1.762H16.423c-.98 0-1.772-.788-1.772-1.761 0-.973.792-1.761 1.772-1.761h14.325zm13.988 0c.98 0 1.772.788 1.772 1.76 0 .974-.792 1.762-1.772 1.762h-5.29a1.768 1.768 0 0 1-1.772-1.761c0-.973.796-1.761 1.772-1.761h5.29zm9.868 0c.977 0 1.773.788 1.773 1.76 0 .974-.796 1.762-1.773 1.762H53.05a1.766 1.766 0 0 1-1.772-1.761c0-.973.793-1.761 1.772-1.761h1.553zm17.107 0c.977 0 1.772.788 1.772 1.76 0 .974-.795 1.762-1.772 1.762h-8.194c-.98 0-1.773-.788-1.773-1.761 0-.973.793-1.761 1.773-1.761h8.194zm12.56-16.238H5.122c-.82 0-1.5.628-1.572 1.424l-.006.143v7.631H85.85V5.09c0-.863-.709-1.567-1.58-1.567zM9.125 6.24c.98 0 1.772.787 1.772 1.76s-.793 1.761-1.772 1.761c-.977 0-1.8-.788-1.8-1.76 0-.974.761-1.761 1.741-1.761h.059zm7.354 0c.979 0 1.772.787 1.772 1.76s-.793 1.761-1.772 1.761c-.977 0-1.829-.788-1.829-1.76 0-.974.734-1.761 1.712-1.761h.117zm7.297 0c.977 0 1.773.787 1.773 1.76s-.796 1.761-1.773 1.761c-.979 0-1.8-.788-1.8-1.76 0-.974.764-1.761 1.743-1.761h.057z" fill="#9B9B9B" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>CVAT</title>
|
||||
|
||||
<script src="./cvat-core.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,15 @@
|
||||
{
|
||||
"short_name": "CVAT",
|
||||
"name": "Computer Vision Annotation Tool",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
server {
|
||||
root /usr/share/nginx/html;
|
||||
# Any route that doesn't have a file extension (e.g. /devices)
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
import { Dispatch, ActionCreator } from 'redux';
|
||||
|
||||
|
||||
export const dumpAnnotation = () => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'DUMP_ANNOTATION',
|
||||
});
|
||||
}
|
||||
|
||||
export const dumpAnnotationSuccess = (downloadLink: string) => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'DUMP_ANNOTATION_SUCCESS',
|
||||
payload: downloadLink,
|
||||
});
|
||||
}
|
||||
|
||||
export const dumpAnnotationError = (error = {}) => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'DUMP_ANNOTATION_ERROR',
|
||||
payload: error,
|
||||
});
|
||||
}
|
||||
|
||||
export const uploadAnnotation = () => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'UPLOAD_ANNOTATION',
|
||||
});
|
||||
}
|
||||
|
||||
export const uploadAnnotationSuccess = () => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'UPLOAD_ANNOTATION_SUCCESS',
|
||||
});
|
||||
}
|
||||
|
||||
export const uploadAnnotationError = (error = {}) => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'UPLOAD_ANNOTATION_ERROR',
|
||||
payload: error,
|
||||
});
|
||||
}
|
||||
|
||||
export const dumpAnnotationAsync = (task: any, dumper: any) => {
|
||||
return (dispatch: ActionCreator<Dispatch>) => {
|
||||
dispatch(dumpAnnotation());
|
||||
|
||||
return task.annotations.dump(task.name, dumper).then(
|
||||
(downloadLink: string) => {
|
||||
dispatch(dumpAnnotationSuccess(downloadLink));
|
||||
},
|
||||
(error: any) => {
|
||||
dispatch(dumpAnnotationError(error));
|
||||
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const uploadAnnotationAsync = (task: any, file: File, loader: any) => {
|
||||
return (dispatch: ActionCreator<Dispatch>) => {
|
||||
dispatch(uploadAnnotation());
|
||||
|
||||
return task.annotations.upload(file, loader).then(
|
||||
(response: any) => {
|
||||
dispatch(uploadAnnotationSuccess());
|
||||
},
|
||||
(error: any) => {
|
||||
dispatch(uploadAnnotationError(error));
|
||||
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,172 @@
|
||||
import { History } from 'history';
|
||||
import { Dispatch, ActionCreator } from 'redux';
|
||||
|
||||
|
||||
export const login = () => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'LOGIN',
|
||||
});
|
||||
}
|
||||
|
||||
export const loginSuccess = () => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'LOGIN_SUCCESS',
|
||||
});
|
||||
}
|
||||
|
||||
export const loginError = (error = {}) => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'LOGIN_ERROR',
|
||||
payload: error,
|
||||
});
|
||||
}
|
||||
|
||||
export const logout = () => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'LOGOUT',
|
||||
});
|
||||
}
|
||||
|
||||
export const logoutSuccess = () => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'LOGOUT_SUCCESS',
|
||||
});
|
||||
}
|
||||
|
||||
export const logoutError = (error = {}) => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'LOGOUT_ERROR',
|
||||
payload: error,
|
||||
});
|
||||
}
|
||||
|
||||
export const isAuthenticated = () => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'IS_AUTHENTICATED',
|
||||
});
|
||||
}
|
||||
|
||||
export const isAuthenticatedSuccess = () => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'IS_AUTHENTICATED_SUCCESS',
|
||||
});
|
||||
}
|
||||
|
||||
export const isAuthenticatedFail = () => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'IS_AUTHENTICATED_FAIL',
|
||||
});
|
||||
}
|
||||
|
||||
export const isAuthenticatedError = (error = {}) => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'IS_AUTHENTICATED_ERROR',
|
||||
payload: error,
|
||||
});
|
||||
}
|
||||
|
||||
export const register = () => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'REGISTER',
|
||||
});
|
||||
}
|
||||
|
||||
export const registerSuccess = () => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'REGISTER_SUCCESS',
|
||||
});
|
||||
}
|
||||
|
||||
export const registerError = (error = {}) => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'REGISTER_ERROR',
|
||||
payload: error,
|
||||
});
|
||||
}
|
||||
|
||||
export const loginAsync = (username: string, password: string, history: History) => {
|
||||
return (dispatch: ActionCreator<Dispatch>) => {
|
||||
dispatch(login());
|
||||
|
||||
return (window as any).cvat.server.login(username, password).then(
|
||||
(loggedIn: any) => {
|
||||
dispatch(loginSuccess());
|
||||
|
||||
history.push(history.location.state ? history.location.state.from : '/tasks');
|
||||
},
|
||||
(error: any) => {
|
||||
dispatch(loginError(error));
|
||||
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const logoutAsync = () => {
|
||||
return (dispatch: ActionCreator<Dispatch>) => {
|
||||
dispatch(logout());
|
||||
|
||||
return (window as any).cvat.server.logout().then(
|
||||
(loggedOut: any) => {
|
||||
dispatch(logoutSuccess());
|
||||
},
|
||||
(error: any) => {
|
||||
dispatch(logoutError(error));
|
||||
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const isAuthenticatedAsync = () => {
|
||||
return (dispatch: ActionCreator<Dispatch>) => {
|
||||
dispatch(isAuthenticated());
|
||||
|
||||
return (window as any).cvat.server.authorized().then(
|
||||
(isAuthenticated: boolean) => {
|
||||
isAuthenticated ? dispatch(isAuthenticatedSuccess()) : dispatch(isAuthenticatedFail());
|
||||
},
|
||||
(error: any) => {
|
||||
dispatch(isAuthenticatedError(error));
|
||||
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const registerAsync = (
|
||||
username: string,
|
||||
firstName: string,
|
||||
lastName: string,
|
||||
email: string,
|
||||
password: string,
|
||||
passwordConfirmation: string,
|
||||
history: History,
|
||||
) => {
|
||||
return (dispatch: ActionCreator<Dispatch>) => {
|
||||
dispatch(register());
|
||||
|
||||
return (window as any).cvat.server.register(
|
||||
username,
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
password,
|
||||
passwordConfirmation,
|
||||
).then(
|
||||
(registered: any) => {
|
||||
dispatch(registerSuccess());
|
||||
|
||||
history.replace('/login');
|
||||
},
|
||||
(error: any) => {
|
||||
dispatch(registerError(error));
|
||||
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
import { Dispatch, ActionCreator } from 'redux';
|
||||
|
||||
|
||||
export const getServerInfo = () => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'GET_SERVER_INFO',
|
||||
});
|
||||
}
|
||||
|
||||
export const getServerInfoSuccess = (information: null) => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'GET_SERVER_INFO_SUCCESS',
|
||||
payload: information,
|
||||
});
|
||||
}
|
||||
|
||||
export const getServerInfoError = (error = {}) => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'GET_SERVER_INFO_ERROR',
|
||||
payload: error,
|
||||
});
|
||||
}
|
||||
|
||||
export const getShareFiles = () => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'GET_SHARE_FILES',
|
||||
});
|
||||
}
|
||||
|
||||
export const getShareFilesSuccess = (files: []) => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'GET_SHARE_FILES_SUCCESS',
|
||||
payload: files,
|
||||
});
|
||||
}
|
||||
|
||||
export const getShareFilesError = (error = {}) => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'GET_SHARE_FILES_ERROR',
|
||||
payload: error,
|
||||
});
|
||||
}
|
||||
|
||||
export const getAnnotationFormats = () => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'GET_ANNOTATION_FORMATS',
|
||||
});
|
||||
}
|
||||
|
||||
export const getAnnotationFormatsSuccess = (annotationFormats: []) => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'GET_ANNOTATION_FORMATS_SUCCESS',
|
||||
payload: annotationFormats,
|
||||
});
|
||||
}
|
||||
|
||||
export const getAnnotationFormatsError = (error = {}) => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'GET_ANNOTATION_FORMATS_ERROR',
|
||||
payload: error,
|
||||
});
|
||||
}
|
||||
|
||||
export const getServerInfoAsync = () => {
|
||||
return (dispatch: ActionCreator<Dispatch>) => {
|
||||
dispatch(getServerInfo());
|
||||
|
||||
return (window as any).cvat.server.about().then(
|
||||
(information: any) => {
|
||||
dispatch(getServerInfoSuccess(information));
|
||||
},
|
||||
(error: any) => {
|
||||
dispatch(getServerInfoError(error));
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const getShareFilesAsync = (directory: string) => {
|
||||
return (dispatch: ActionCreator<Dispatch>) => {
|
||||
dispatch(getShareFiles());
|
||||
|
||||
return (window as any).cvat.server.share(directory).then(
|
||||
(files: any) => {
|
||||
dispatch(getShareFilesSuccess(files));
|
||||
},
|
||||
(error: any) => {
|
||||
dispatch(getShareFilesError(error));
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const getAnnotationFormatsAsync = () => {
|
||||
return (dispatch: ActionCreator<Dispatch>) => {
|
||||
dispatch(getAnnotationFormats());
|
||||
|
||||
return (window as any).cvat.server.formats().then(
|
||||
(formats: any) => {
|
||||
dispatch(getAnnotationFormatsSuccess(formats));
|
||||
},
|
||||
(error: any) => {
|
||||
dispatch(getAnnotationFormatsError(error));
|
||||
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
import { Dispatch } from 'redux';
|
||||
|
||||
|
||||
export const filterTasks = (queryParams: { search?: string, page?: number }) => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'FILTER_TASKS',
|
||||
payload: queryParams,
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,175 @@
|
||||
import { History } from 'history';
|
||||
import { Dispatch, ActionCreator } from 'redux';
|
||||
|
||||
import queryString from 'query-string';
|
||||
|
||||
import setQueryObject from '../utils/tasks-filter'
|
||||
|
||||
|
||||
export const getTasks = () => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'GET_TASKS',
|
||||
});
|
||||
}
|
||||
|
||||
export const getTasksSuccess = (tasks: []) => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'GET_TASKS_SUCCESS',
|
||||
payload: tasks,
|
||||
});
|
||||
}
|
||||
|
||||
export const getTasksError = (error: {}) => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'GET_TASKS_ERROR',
|
||||
payload: error,
|
||||
});
|
||||
}
|
||||
|
||||
export const createTask = () => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'CREATE_TASK',
|
||||
});
|
||||
}
|
||||
|
||||
export const createTaskSuccess = () => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'CREATE_TASK_SUCCESS',
|
||||
});
|
||||
}
|
||||
|
||||
export const createTaskError = (error: {}) => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'CREATE_TASK_ERROR',
|
||||
payload: error,
|
||||
});
|
||||
}
|
||||
|
||||
export const updateTask = () => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_TASK',
|
||||
});
|
||||
}
|
||||
|
||||
export const updateTaskSuccess = () => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_TASK_SUCCESS',
|
||||
});
|
||||
}
|
||||
|
||||
export const updateTaskError = (error: {}) => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_TASK_ERROR',
|
||||
payload: error,
|
||||
});
|
||||
}
|
||||
|
||||
export const deleteTask = () => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'DELETE_TASK',
|
||||
});
|
||||
}
|
||||
|
||||
export const deleteTaskSuccess = () => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'DELETE_TASK_SUCCESS',
|
||||
});
|
||||
}
|
||||
|
||||
export const deleteTaskError = (error: {}) => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'DELETE_TASK_ERROR',
|
||||
payload: error,
|
||||
});
|
||||
}
|
||||
|
||||
export const getTasksAsync = (queryObject = {}) => {
|
||||
return (dispatch: ActionCreator<Dispatch>) => {
|
||||
dispatch(getTasks());
|
||||
|
||||
return (window as any).cvat.tasks.get(queryObject).then(
|
||||
(tasks: any) => {
|
||||
dispatch(getTasksSuccess(tasks));
|
||||
},
|
||||
(error: any) => {
|
||||
dispatch(getTasksError(error));
|
||||
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const createTaskAsync = (task: any) => {
|
||||
return (dispatch: ActionCreator<Dispatch>) => {
|
||||
dispatch(createTask());
|
||||
|
||||
return task.save().then(
|
||||
(created: any) => {
|
||||
dispatch(createTaskSuccess());
|
||||
|
||||
return dispatch(getTasksAsync());
|
||||
},
|
||||
(error: any) => {
|
||||
dispatch(createTaskError(error));
|
||||
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const updateTaskAsync = (task: any) => {
|
||||
return (dispatch: ActionCreator<Dispatch>) => {
|
||||
dispatch(updateTask());
|
||||
|
||||
return task.save().then(
|
||||
(updated: any) => {
|
||||
dispatch(updateTaskSuccess());
|
||||
|
||||
return dispatch(getTasksAsync());
|
||||
},
|
||||
(error: any) => {
|
||||
dispatch(updateTaskError(error));
|
||||
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const deleteTaskAsync = (task: any, history: History) => {
|
||||
return (dispatch: ActionCreator<Dispatch>, getState: any) => {
|
||||
dispatch(deleteTask());
|
||||
|
||||
return task.delete().then(
|
||||
(deleted: any) => {
|
||||
dispatch(deleteTaskSuccess());
|
||||
|
||||
const state = getState();
|
||||
|
||||
const queryObject = {
|
||||
page: state.tasksFilter.currentPage,
|
||||
search: state.tasksFilter.searchQuery,
|
||||
}
|
||||
|
||||
if (state.tasks.tasks.length === 1 && state.tasks.tasksCount !== 1) {
|
||||
queryObject.page = queryObject.page - 1;
|
||||
|
||||
history.push({ search: queryString.stringify(queryObject) });
|
||||
} else if (state.tasks.tasksCount === 1) {
|
||||
return dispatch(getTasksAsync());
|
||||
} else {
|
||||
const query = setQueryObject(queryObject);
|
||||
|
||||
return dispatch(getTasksAsync(query));
|
||||
}
|
||||
},
|
||||
(error: any) => {
|
||||
dispatch(deleteTaskError(error));
|
||||
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
import { Dispatch, ActionCreator } from 'redux';
|
||||
|
||||
|
||||
export const getUsers = () => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'GET_USERS',
|
||||
});
|
||||
}
|
||||
|
||||
export const getUsersSuccess = (users: [], isCurrentUser: boolean) => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'GET_USERS_SUCCESS',
|
||||
payload: users,
|
||||
currentUser: isCurrentUser ? (users as any)[0] : isCurrentUser,
|
||||
});
|
||||
}
|
||||
|
||||
export const getUsersError = (error: {}) => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: 'GET_USERS_ERROR',
|
||||
payload: error,
|
||||
});
|
||||
}
|
||||
|
||||
export const getUsersAsync = (filter = {}) => {
|
||||
return (dispatch: ActionCreator<Dispatch>) => {
|
||||
dispatch(getUsers());
|
||||
|
||||
return (window as any).cvat.users.get(filter).then(
|
||||
(users: any) => {
|
||||
dispatch(getUsersSuccess(users, (filter as any).self));
|
||||
},
|
||||
(error: any) => {
|
||||
dispatch(getUsersError(error));
|
||||
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
.App {
|
||||
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import App from './app';
|
||||
|
||||
|
||||
it('renders without crashing', () => {
|
||||
const div = document.createElement('div');
|
||||
ReactDOM.render(<App />, div);
|
||||
ReactDOM.unmountComponentAtNode(div);
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue