Merge branch 'release-1.2.0'

main
Nikita Manovich 5 years ago
commit 37d82f9005

@ -1,5 +1,5 @@
exclude_paths: exclude_paths:
- '**/3rdparty/**' - '**/3rdparty/**'
- '**/engine/js/cvat-core.min.js' - '**/engine/js/cvat-core.min.js'
- '**/engine/js/unzip_imgs.js' - '**/engine/js/unzip_imgs.js'
- CHANGELOG.md - CHANGELOG.md

@ -3,13 +3,10 @@ branch = true
# relative_files = true # does not work? # relative_files = true # does not work?
source = source =
datumaro/datumaro/
cvat/apps/ cvat/apps/
utils/cli/ utils/cli/
omit = omit =
datumaro/datumaro/__main__.py
datumaro/datumaro/version.py
cvat/settings/* cvat/settings/*
*/tests/* */tests/*
*/test_* */test_*

@ -0,0 +1,10 @@
.*/
3rdparty/
node_modules/
dist/
data/
datumaro/
keys/
logs/
static/
templates/

@ -1,53 +1,23 @@
/* // Copyright (C) 2018-2020 Intel Corporation
* Copyright (C) 2018 Intel Corporation //
* // SPDX-License-Identifier: MIT
* SPDX-License-Identifier: MIT
*/
module.exports = { module.exports = {
"env": { env: {
"node": false, node: true,
"browser": true, browser: true,
"es6": true, es6: true,
"jquery": true,
"qunit": true,
}, },
"parserOptions": { parserOptions: {
"sourceType": "script", sourceType: 'module',
ecmaVersion: 2018,
}, },
"plugins": [ plugins: ['eslint-plugin-header'],
"security", extends: ['eslint:recommended', 'prettier'],
"no-unsanitized", rules: {
"no-unsafe-innerhtml", 'header/header': [2, 'line', [{
], pattern: ' {1}Copyright \\(C\\) (?:20\\d{2}-)?2020 Intel Corporation',
"extends": [ template: ' Copyright (C) 2020 Intel Corporation'
"eslint:recommended", }, '', ' SPDX-License-Identifier: MIT']],
"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-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],
// recently added to airbnb
"max-classes-per-file": [0],
// it was opposite before and our code has been written according to previous rule
"arrow-parens": [0],
// object spread is a modern ECMA standard. Let's do not use it without babel
"prefer-object-spread": [0],
}, },
}; };

4
.gitignore vendored

@ -30,3 +30,7 @@ yarn-debug.log*
yarn-error.log* yarn-error.log*
.DS_Store .DS_Store
#Ignore Cypress tests temp files
/tests/cypress/fixtures
/tests/cypress/screenshots

@ -0,0 +1,17 @@
{
"all": true,
"compact": false,
"extension": [
".js",
".jsx",
".ts",
".tsx"
],
"exclude": [
"**/3rdparty/*",
"**/tests/*"
],
"parser-plugins": [
"typescript"
]
}

@ -0,0 +1,10 @@
.*/
3rdparty/
node_modules/
dist/
data/
datumaro/
keys/
logs/
static/
templates/

@ -0,0 +1,27 @@
{
"arrowParens": "always",
"bracketSpacing": true,
"embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxBracketSameLine": false,
"jsxSingleQuote": true,
"printWidth": 120,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleQuote": true,
"tabWidth": 4,
"trailingComma": "all",
"useTabs": false,
"vueIndentScriptAndStyle": false,
"overrides": [
{
"files": ["*.json", "*.yml", "*.yaml", "*.md"],
"options": {
"tabWidth": 2
}
}
]
}

@ -1,16 +1,16 @@
exports.settings = {bullet: '*', paddedTable: false} exports.settings = { bullet: '*', paddedTable: false };
exports.plugins = [ exports.plugins = [
'remark-preset-lint-recommended', 'remark-preset-lint-recommended',
'remark-preset-lint-consistent', 'remark-preset-lint-consistent',
['remark-preset-lint-markdown-style-guide', 'mixed'], ['remark-preset-lint-markdown-style-guide', 'mixed'],
['remark-lint-no-dead-urls', { skipOffline: true }], ['remark-lint-no-dead-urls', { skipOffline: true }],
['remark-lint-maximum-line-length', 120], ['remark-lint-maximum-line-length', 120],
['remark-lint-maximum-heading-length', 120], ['remark-lint-maximum-heading-length', 120],
['remark-lint-strong-marker', "*"], ['remark-lint-strong-marker', '*'],
['remark-lint-emphasis-marker', "_"], ['remark-lint-emphasis-marker', '_'],
['remark-lint-unordered-list-marker-style', "-"], ['remark-lint-unordered-list-marker-style', '-'],
['remark-lint-ordered-list-marker-style', "."], ['remark-lint-ordered-list-marker-style', '.'],
['remark-lint-no-file-name-irregular-characters', false], ['remark-lint-no-file-name-irregular-characters', false],
['remark-lint-list-item-spacing', false], ['remark-lint-list-item-spacing', false],
] ];

@ -1,20 +1,22 @@
{ {
"extends": "stylelint-config-standard", "extends": "stylelint-config-standard",
"rules": { "rules": {
"indentation": 4, "indentation": 4,
"value-keyword-case": null, "value-keyword-case": null,
"selector-combinator-space-after": null, "selector-combinator-space-after": null,
"no-descending-specificity": null, "no-descending-specificity": null,
"at-rule-no-unknown": [true, { "at-rule-no-unknown": [
"ignoreAtRules": ["extend"] true,
}], {
"selector-type-no-unknown": [true, { "ignoreAtRules": ["extend"]
"ignoreTypes": ["first-child"] }
}] ],
}, "selector-type-no-unknown": [
"ignoreFiles": [ true,
"**/*.js", {
"**/*.ts", "ignoreTypes": ["first-child"]
"**/*.py" }
] ]
},
"ignoreFiles": ["**/*.js", "**/*.ts", "**/*.py"]
} }

@ -1,9 +1,5 @@
sudo: required language: generic
dist: focal
language: python
python:
- "3.5"
cache: cache:
npm: true npm: true
@ -11,9 +7,11 @@ cache:
- ~/.cache - ~/.cache
addons: addons:
firefox: 'latest'
chrome: stable
apt: apt:
packages: packages:
- libgconf-2-4 - libgconf-2-4
services: services:
- docker - docker
@ -25,25 +23,34 @@ env:
DJANGO_SU_EMAIL="admin@localhost.company" DJANGO_SU_EMAIL="admin@localhost.company"
DJANGO_SU_PASSWORD="12qwaszx" DJANGO_SU_PASSWORD="12qwaszx"
NODE_VERSION="12" NODE_VERSION="12"
API_ABOUT_PAGE="localhost:8080/api/v1/server/about"
before_install: before_install:
- nvm install ${NODE_VERSION} - nvm install ${NODE_VERSION}
before_script: before_script:
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml build
- chmod a+rwx ${HOST_COVERAGE_DATA_DIR} - chmod a+rwx ${HOST_COVERAGE_DATA_DIR}
script: script:
# FIXME: Git package and application name conflict in PATH and try to leave only one python test execution - if [[ $TRAVIS_EVENT_TYPE == "cron" && $TRAVIS_BRANCH == "develop" ]];
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'coverage run -a manage.py test cvat/apps && coverage run -a manage.py test --pattern="_test*.py" cvat/apps/dataset_manager/tests cvat/apps/engine/tests utils/cli && coverage run -a manage.py test datumaro/ && mv .coverage ${CONTAINER_COVERAGE_DATA_DIR}' then
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'cd cvat-data && npm install && cd ../cvat-core && npm install && npm run test && coveralls-lcov -v -n ./reports/coverage/lcov.info > ${CONTAINER_COVERAGE_DATA_DIR}/coverage.json' docker-compose -f docker-compose.yml -f ./tests/docker-compose.email.yml up -d --build;
# Up all containers bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' ${API_ABOUT_PAGE})" != "401" ]]; do sleep 5; done';
docker exec -it cvat bash -ic "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell";
cd ./tests && npm install && npm run cypress:run:firefox; exit $?;
fi;
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml build
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'coverage run -a manage.py test cvat/apps utils/cli && mv .coverage ${CONTAINER_COVERAGE_DATA_DIR}'
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'cd cvat-data && npm install && cd ../cvat-core && npm install && npm run test && mv ./reports/coverage/lcov.info ${CONTAINER_COVERAGE_DATA_DIR} && chmod a+rwx ${CONTAINER_COVERAGE_DATA_DIR}/lcov.info'
- docker-compose up -d - docker-compose up -d
# Create superuser
- docker exec -it cvat bash -ic "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell" - docker exec -it cvat bash -ic "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell"
# Install Cypress and run tests # End-to-end testing
- cd ./tests && npm install - npm install && npm run coverage
- $(npm bin)/cypress run --headless --browser chrome && cd .. - docker-compose up -d --build
- cd ./tests && npm install && npx cypress run --headless --browser chrome
- mv ./.nyc_output ../ && cd ..
- npx nyc report --reporter=text-lcov >> ${HOST_COVERAGE_DATA_DIR}/lcov.info
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'cd ${CONTAINER_COVERAGE_DATA_DIR} && coveralls-lcov -v -n lcov.info > ${CONTAINER_COVERAGE_DATA_DIR}/coverage.json'
after_success: after_success:
# https://coveralls-python.readthedocs.io/en/latest/usage/multilang.html # https://coveralls-python.readthedocs.io/en/latest/usage/multilang.html

@ -21,6 +21,21 @@
}, },
"smartStep": true, "smartStep": true,
}, },
{
"type": "node",
"request": "launch",
"name": "ui.js: test",
"cwd": "${workspaceRoot}/tests",
"runtimeExecutable": "${workspaceRoot}/tests/node_modules/.bin/cypress",
"args": [
"run",
"--headless",
"--browser",
"chrome"
],
"outputCapture": "std",
"console": "internalConsole"
},
{ {
"name": "server: django", "name": "server: django",
"type": "python", "type": "python",

@ -1 +0,0 @@
PYTHONPATH="datumaro/:$PYTHONPATH"

@ -1,37 +1,26 @@
{ {
"python.pythonPath": ".env/bin/python", "python.pythonPath": ".env/bin/python",
"eslint.enable": true, "eslint.enable": true,
"eslint.validate": [ "eslint.probe": [
"javascript", "javascript",
"typescript", "typescript",
"typescriptreact", "typescriptreact"
], ],
"eslint.onIgnoredFiles": "warn",
"eslint.workingDirectories": [ "eslint.workingDirectories": [
{ {
"directory": "./cvat-core", "directory": "${cwd}",
"changeProcessCWD": true
}, },
{ {
"directory": "./cvat-canvas", "pattern": "cvat-*"
"changeProcessCWD": true
}, },
{ {
"directory": "./cvat-ui", "directory": "tests",
"changeProcessCWD": true "!cwd": true
},
{
"directory": ".",
"changeProcessCWD": true
} }
], ],
"python.linting.pylintEnabled": true, "python.linting.pylintEnabled": true,
"python.envFile": "${workspaceFolder}/.vscode/python.env",
"python.testing.unittestEnabled": true, "python.testing.unittestEnabled": true,
"python.testing.unittestArgs": [
"-v",
"-s",
"./datumaro",
],
"licenser.license": "Custom", "licenser.license": "Custom",
"licenser.customHeader": "Copyright (C) @YEAR@ Intel Corporation\n\nSPDX-License-Identifier: MIT", "licenser.customHeader": "Copyright (C) @YEAR@ Intel Corporation\n\nSPDX-License-Identifier: MIT",
"files.trimTrailingWhitespace": true "files.trimTrailingWhitespace": true

@ -1,11 +1,127 @@
# Changelog # Changelog
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.2.0] - 2020-01-08
### Fixed
- Memory consumption for the task creation process (<https://github.com/openvinotoolkit/cvat/pull/2582>)
- Frame preloading (<https://github.com/openvinotoolkit/cvat/pull/2608>)
- Project cannot be removed from the project page (<https://github.com/openvinotoolkit/cvat/pull/2626>)
## [1.2.0-beta] - 2020-12-15
### Added
- GPU support and improved documentation for auto annotation (<https://github.com/openvinotoolkit/cvat/pull/2546>)
- Manual review pipeline: issues/comments/workspace (<https://github.com/openvinotoolkit/cvat/pull/2357>)
- Basic projects implementation (<https://github.com/openvinotoolkit/cvat/pull/2255>)
- Documentation on how to mount cloud starage(AWS S3 bucket, Azure container, Google Drive) as FUSE (<https://github.com/openvinotoolkit/cvat/pull/2377>)
- Ability to work with share files without copying inside (<https://github.com/openvinotoolkit/cvat/pull/2377>)
- Tooltips in label selectors (<https://github.com/openvinotoolkit/cvat/pull/2509>)
- Page redirect after login using `next` query parameter (<https://github.com/openvinotoolkit/cvat/pull/2527>)
- [ImageNet](http://www.image-net.org) format support (<https://github.com/openvinotoolkit/cvat/pull/2376>)
- [CamVid](http://mi.eng.cam.ac.uk/research/projects/VideoRec/CamVid/) format support (<https://github.com/openvinotoolkit/cvat/pull/2559>)
### Changed
- PATCH requests from cvat-core submit only changed fields (<https://github.com/openvinotoolkit/cvat/pull/2445>)
- deploy.sh in serverless folder is seperated into deploy_cpu.sh and deploy_gpu.sh (<https://github.com/openvinotoolkit/cvat/pull/2546>)
- Bumped nuclio version to 1.5.8
- Migrated to Antd 4.9 (<https://github.com/openvinotoolkit/cvat/pull/2536>)
### Fixed
- Fixed FastRCNN inference bug for images with 4 channels i.e. png (<https://github.com/openvinotoolkit/cvat/pull/2546>)
- Django templates for email and user guide (<https://github.com/openvinotoolkit/cvat/pull/2412>)
- Saving relative paths in dummy chunks instead of absolute (<https://github.com/openvinotoolkit/cvat/pull/2424>)
- Objects with a specific label cannot be displayed if at least one tag with the label exist (<https://github.com/openvinotoolkit/cvat/pull/2435>)
- Wrong attribute can be removed in labels editor (<https://github.com/openvinotoolkit/cvat/pull/2436>)
- UI fails with the error "Cannot read property 'label' of undefined" (<https://github.com/openvinotoolkit/cvat/pull/2442>)
- Exception: "Value must be a user instance" (<https://github.com/openvinotoolkit/cvat/pull/2441>)
- Reset zoom option doesn't work in tag annotation mode (<https://github.com/openvinotoolkit/cvat/pull/2443>)
- Canvas is busy error (<https://github.com/openvinotoolkit/cvat/pull/2437>)
- Projects view layout fix (<https://github.com/openvinotoolkit/cvat/pull/2503>)
- Fixed the tasks view (infinite loading) when it is impossible to get a preview of the task (<https://github.com/openvinotoolkit/cvat/pull/2504>)
- Empty frames navigation (<https://github.com/openvinotoolkit/cvat/pull/2505>)
- TypeError: Cannot read property 'toString' of undefined (<https://github.com/openvinotoolkit/cvat/pull/2517>)
- Extra shapes are drawn after Esc, or G pressed while drawing a region in grouping (<https://github.com/openvinotoolkit/cvat/pull/2507>)
- Reset state (reviews, issues) after logout or changing a job (<https://github.com/openvinotoolkit/cvat/pull/2525>)
- TypeError: Cannot read property 'id' of undefined when updating a task (<https://github.com/openvinotoolkit/cvat/pull/2544>)
## [1.2.0-alpha] - 2020-11-09
### Added
- Ability to login into CVAT-UI with token from api/v1/auth/login (<https://github.com/openvinotoolkit/cvat/pull/2234>)
- Added layout grids toggling ('ctrl + alt + Enter')
- Added password reset functionality (<https://github.com/opencv/cvat/pull/2058>)
- Ability to work with data on the fly (https://github.com/opencv/cvat/pull/2007)
- Annotation in process outline color wheel (<https://github.com/opencv/cvat/pull/2084>)
- On the fly annotation using DL detectors (<https://github.com/opencv/cvat/pull/2102>)
- Displaying automatic annotation progress on a task view (<https://github.com/opencv/cvat/pull/2148>)
- Automatic tracking of bounding boxes using serverless functions (<https://github.com/opencv/cvat/pull/2136>)
- [Datumaro] CLI command for dataset equality comparison (<https://github.com/opencv/cvat/pull/1989>)
- [Datumaro] Merging of datasets with different labels (<https://github.com/opencv/cvat/pull/2098>)
- Add FBRS interactive segmentation serverless function (<https://github.com/openvinotoolkit/cvat/pull/2094>)
- Ability to change default behaviour of previous/next buttons of a player.
It supports regular navigation, searching a frame according to annotations
filters and searching the nearest frame without any annotations (<https://github.com/openvinotoolkit/cvat/pull/2221>)
- MacOS users notes in CONTRIBUTING.md
- Ability to prepare meta information manually (<https://github.com/openvinotoolkit/cvat/pull/2217>)
- Ability to upload prepared meta information along with a video when creating a task (<https://github.com/openvinotoolkit/cvat/pull/2217>)
- Optional chaining plugin for cvat-canvas and cvat-ui (<https://github.com/openvinotoolkit/cvat/pull/2249>)
- MOTS png mask format support (<https://github.com/openvinotoolkit/cvat/pull/2198>)
- Ability to correct upload video with a rotation record in the metadata (<https://github.com/openvinotoolkit/cvat/pull/2218>)
- User search field for assignee fields (<https://github.com/openvinotoolkit/cvat/pull/2370>)
- Support of mxf videos (<https://github.com/openvinotoolkit/cvat/pull/2514>)
### Changed
- UI models (like DEXTR) were redesigned to be more interactive (<https://github.com/opencv/cvat/pull/2054>)
- Used Ubuntu:20.04 as a base image for CVAT Dockerfile (<https://github.com/opencv/cvat/pull/2101>)
- Right colors of label tags in label mapping when a user runs automatic detection (<https://github.com/openvinotoolkit/cvat/pull/2162>)
- Nuclio became an optional component of CVAT (<https://github.com/openvinotoolkit/cvat/pull/2192>)
- A key to remove a point from a polyshape [Ctrl => Alt] (<https://github.com/openvinotoolkit/cvat/pull/2204>)
- Updated `docker-compose` file version from `2.3` to `3.3`(<https://github.com/openvinotoolkit/cvat/pull/2235>)
- Added auto inference of url schema from host in CLI, if provided (<https://github.com/openvinotoolkit/cvat/pull/2240>)
- Track frames in skips between annotation is presented in MOT and MOTS formats are marked `outside` (<https://github.com/openvinotoolkit/cvat/pull/2198>)
- UI packages installation with `npm ci` instead of `npm install` (<https://github.com/openvinotoolkit/cvat/pull/2350>)
### Removed
- Removed Z-Order flag from task creation process
### Fixed
- Fixed multiple errors which arises when polygon is of length 5 or less (<https://github.com/opencv/cvat/pull/2100>)
- Fixed task creation from PDF (<https://github.com/opencv/cvat/pull/2141>)
- Fixed CVAT format import for frame stepped tasks (<https://github.com/openvinotoolkit/cvat/pull/2151>)
- Fixed the reading problem with large PDFs (<https://github.com/openvinotoolkit/cvat/pull/2154>)
- Fixed unnecessary pyhash dependency (<https://github.com/openvinotoolkit/cvat/pull/2170>)
- Fixed Data is not getting cleared, even after deleting the Task from Django Admin App(<https://github.com/openvinotoolkit/cvat/issues/1925>)
- Fixed blinking message: "Some tasks have not been showed because they do not have any data" (<https://github.com/openvinotoolkit/cvat/pull/2200>)
- Fixed case when a task with 0 jobs is shown as "Completed" in UI (<https://github.com/openvinotoolkit/cvat/pull/2200>)
- Fixed use case when UI throws exception: Cannot read property 'objectType' of undefined #2053 (<https://github.com/openvinotoolkit/cvat/pull/2203>)
- Fixed use case when logs could be saved twice or more times #2202 (<https://github.com/openvinotoolkit/cvat/pull/2203>)
- Fixed issues from #2112 (<https://github.com/openvinotoolkit/cvat/pull/2217>)
- Git application name (renamed to dataset_repo) (<https://github.com/openvinotoolkit/cvat/pull/2243>)
- A problem in exporting of tracks, where tracks could be truncated (<https://github.com/openvinotoolkit/cvat/issues/2129>)
- Fixed CVAT startup process if the user has `umask 077` in .bashrc file (<https://github.com/openvinotoolkit/cvat/pull/2293>)
- Exception: Cannot read property "each" of undefined after drawing a single point (<https://github.com/openvinotoolkit/cvat/pull/2307>)
- Cannot read property 'label' of undefined (Fixed?) (<https://github.com/openvinotoolkit/cvat/pull/2311>)
- Excluded track frames marked `outside` in `CVAT for Images` export (<https://github.com/openvinotoolkit/cvat/pull/2345>)
- 'List of tasks' Kibana visualization (<https://github.com/openvinotoolkit/cvat/pull/2361>)
- An error on exporting not `jpg` or `png` images in TF Detection API format (<https://github.com/openvinotoolkit/datumaro/issues/35>)
## [1.1.0] - 2020-08-31 ## [1.1.0] - 2020-08-31
### Added ### Added
- Siammask tracker as DL serverless function (<https://github.com/opencv/cvat/pull/1988>) - Siammask tracker as DL serverless function (<https://github.com/opencv/cvat/pull/1988>)
- [Datumaro] Added model info and source info commands (<https://github.com/opencv/cvat/pull/1973>) - [Datumaro] Added model info and source info commands (<https://github.com/opencv/cvat/pull/1973>)
- [Datumaro] Dataset statistics (<https://github.com/opencv/cvat/pull/1668>) - [Datumaro] Dataset statistics (<https://github.com/opencv/cvat/pull/1668>)
@ -16,18 +132,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Notification message when users use wrong browser (<https://github.com/opencv/cvat/pull/2070>) - Notification message when users use wrong browser (<https://github.com/opencv/cvat/pull/2070>)
### Changed ### Changed
- Shape coordinates are rounded to 2 digits in dumped annotations (<https://github.com/opencv/cvat/pull/1970>) - Shape coordinates are rounded to 2 digits in dumped annotations (<https://github.com/opencv/cvat/pull/1970>)
- COCO format does not produce polygon points for bbox annotations (<https://github.com/opencv/cvat/pull/1953>) - COCO format does not produce polygon points for bbox annotations (<https://github.com/opencv/cvat/pull/1953>)
### Fixed ### Fixed
- Issue loading openvino models for semi-automatic and automatic annotation (<https://github.com/opencv/cvat/pull/1996>) - Issue loading openvino models for semi-automatic and automatic annotation (<https://github.com/opencv/cvat/pull/1996>)
- Basic functions of CVAT works without activated nuclio dashboard - Basic functions of CVAT works without activated nuclio dashboard
- Fixed a case in which exported masks could have wrong color order (<https://github.com/opencv/cvat/issues/2032>) - Fixed a case in which exported masks could have wrong color order (<https://github.com/opencv/cvat/issues/2032>)
- Fixed error with creating task with labels with the same name (<https://github.com/opencv/cvat/pull/2031>) - Fixed error with creating task with labels with the same name (<https://github.com/opencv/cvat/pull/2031>)
- Django RQ dashboard view (<https://github.com/opencv/cvat/pull/2069>) - Django RQ dashboard view (<https://github.com/opencv/cvat/pull/2069>)
- Object's details menu settings (<https://github.com/opencv/cvat/pull/2084>)
## [1.1.0-beta] - 2020-08-03 ## [1.1.0-beta] - 2020-08-03
### Added ### Added
- DL models as serverless functions (<https://github.com/opencv/cvat/pull/1767>) - DL models as serverless functions (<https://github.com/opencv/cvat/pull/1767>)
- Source type support for tags, shapes and tracks (<https://github.com/opencv/cvat/pull/1192>) - Source type support for tags, shapes and tracks (<https://github.com/opencv/cvat/pull/1192>)
- Source type support for CVAT Dumper/Loader (<https://github.com/opencv/cvat/pull/1192>) - Source type support for CVAT Dumper/Loader (<https://github.com/opencv/cvat/pull/1192>)
@ -38,16 +159,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ability to change user password (<https://github.com/opencv/cvat/pull/1954>) - Ability to change user password (<https://github.com/opencv/cvat/pull/1954>)
### Changed ### Changed
- Smaller object details (<https://github.com/opencv/cvat/pull/1877>) - Smaller object details (<https://github.com/opencv/cvat/pull/1877>)
- `COCO` format does not convert bboxes to polygons on export (<https://github.com/opencv/cvat/pull/1953>) - `COCO` format does not convert bboxes to polygons on export (<https://github.com/opencv/cvat/pull/1953>)
- It is impossible to submit a DL model in OpenVINO format using UI. Now you can deploy new models on the server using serverless functions (<https://github.com/opencv/cvat/pull/1767>) - It is impossible to submit a DL model in OpenVINO format using UI. Now you can deploy new models on the server using serverless functions (<https://github.com/opencv/cvat/pull/1767>)
- Files and folders under share path are now alphabetically sorted - Files and folders under share path are now alphabetically sorted
### Removed ### Removed
- Removed OpenVINO and CUDA components because they are not necessary anymore (<https://github.com/opencv/cvat/pull/1767>) - Removed OpenVINO and CUDA components because they are not necessary anymore (<https://github.com/opencv/cvat/pull/1767>)
- Removed the old UI code (<https://github.com/opencv/cvat/pull/1964>) - Removed the old UI code (<https://github.com/opencv/cvat/pull/1964>)
### Fixed ### Fixed
- Some objects aren't shown on canvas sometimes. For example after propagation on of objects is invisible (<https://github.com/opencv/cvat/pull/1834>) - Some objects aren't shown on canvas sometimes. For example after propagation on of objects is invisible (<https://github.com/opencv/cvat/pull/1834>)
- CVAT doesn't offer to restore state after an error (<https://github.com/opencv/cvat/pull/1874>) - CVAT doesn't offer to restore state after an error (<https://github.com/opencv/cvat/pull/1874>)
- Cannot read property 'shapeType' of undefined because of zOrder related issues (<https://github.com/opencv/cvat/pull/1874>) - Cannot read property 'shapeType' of undefined because of zOrder related issues (<https://github.com/opencv/cvat/pull/1874>)
@ -68,7 +192,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Increase rate of throttling policy for unauthenticated users (<https://github.com/opencv/cvat/pull/1969>) - Increase rate of throttling policy for unauthenticated users (<https://github.com/opencv/cvat/pull/1969>)
## [1.1.0-alpha] - 2020-06-30 ## [1.1.0-alpha] - 2020-06-30
### Added ### Added
- Throttling policy for unauthenticated users (<https://github.com/opencv/cvat/pull/1531>) - Throttling policy for unauthenticated users (<https://github.com/opencv/cvat/pull/1531>)
- Added default label color table for mask export (<https://github.com/opencv/cvat/pull/1549>) - Added default label color table for mask export (<https://github.com/opencv/cvat/pull/1549>)
- Added environment variables for Redis and Postgres hosts for Kubernetes deployment support (<https://github.com/opencv/cvat/pull/1641>) - Added environment variables for Redis and Postgres hosts for Kubernetes deployment support (<https://github.com/opencv/cvat/pull/1641>)
@ -96,6 +222,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [Datumaro] Added image copying when exporting datasets, if possible (<https://github.com/opencv/cvat/pull/1799>) - [Datumaro] Added image copying when exporting datasets, if possible (<https://github.com/opencv/cvat/pull/1799>)
### Changed ### Changed
- Removed information about e-mail from the basic user information (<https://github.com/opencv/cvat/pull/1627>) - Removed information about e-mail from the basic user information (<https://github.com/opencv/cvat/pull/1627>)
- Update https install manual. Makes it easier and more robust. Includes automatic renewing of lets encrypt certificates. - Update https install manual. Makes it easier and more robust. Includes automatic renewing of lets encrypt certificates.
- Settings page move to the modal. (<https://github.com/opencv/cvat/pull/1705>) - Settings page move to the modal. (<https://github.com/opencv/cvat/pull/1705>)
@ -107,6 +234,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [Datumaro] Annotation-less files are not generated anymore in COCO format, unless tasks explicitly requested (<https://github.com/opencv/cvat/pull/1799>) - [Datumaro] Annotation-less files are not generated anymore in COCO format, unless tasks explicitly requested (<https://github.com/opencv/cvat/pull/1799>)
### Fixed ### Fixed
- Problem with exported frame stepped image task (<https://github.com/opencv/cvat/issues/1613>) - Problem with exported frame stepped image task (<https://github.com/opencv/cvat/issues/1613>)
- Fixed dataset filter item representation for imageless dataset items (<https://github.com/opencv/cvat/pull/1593>) - Fixed dataset filter item representation for imageless dataset items (<https://github.com/opencv/cvat/pull/1593>)
- Fixed interpreter crash when trying to import `tensorflow` with no AVX instructions available (<https://github.com/opencv/cvat/pull/1567>) - Fixed interpreter crash when trying to import `tensorflow` with no AVX instructions available (<https://github.com/opencv/cvat/pull/1567>)
@ -127,10 +255,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Error when interpolating polygons (<https://github.com/opencv/cvat/pull/1878>) - Error when interpolating polygons (<https://github.com/opencv/cvat/pull/1878>)
### Security ### Security
- SQL injection in Django `CVE-2020-9402` (<https://github.com/opencv/cvat/pull/1657>) - SQL injection in Django `CVE-2020-9402` (<https://github.com/opencv/cvat/pull/1657>)
## [1.0.0] - 2020-05-29 ## [1.0.0] - 2020-05-29
### Added ### Added
- cvat-ui: cookie policy drawer for login page (<https://github.com/opencv/cvat/pull/1511>) - cvat-ui: cookie policy drawer for login page (<https://github.com/opencv/cvat/pull/1511>)
- `datumaro_project` export format (<https://github.com/opencv/cvat/pull/1352>) - `datumaro_project` export format (<https://github.com/opencv/cvat/pull/1352>)
- Ability to configure user agreements for the user registration form (<https://github.com/opencv/cvat/pull/1464>) - Ability to configure user agreements for the user registration form (<https://github.com/opencv/cvat/pull/1464>)
@ -139,6 +270,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ability to configure access to the analytics page based on roles (<https://github.com/opencv/cvat/pull/1592>) - Ability to configure access to the analytics page based on roles (<https://github.com/opencv/cvat/pull/1592>)
### Changed ### Changed
- Downloaded file name in annotations export became more informative (<https://github.com/opencv/cvat/pull/1352>) - Downloaded file name in annotations export became more informative (<https://github.com/opencv/cvat/pull/1352>)
- Added auto trimming for trailing whitespaces style enforcement (<https://github.com/opencv/cvat/pull/1352>) - Added auto trimming for trailing whitespaces style enforcement (<https://github.com/opencv/cvat/pull/1352>)
- REST API: updated `GET /task/<id>/annotations`: parameters are `format`, `filename` (now optional), `action` (optional) (<https://github.com/opencv/cvat/pull/1352>) - REST API: updated `GET /task/<id>/annotations`: parameters are `format`, `filename` (now optional), `action` (optional) (<https://github.com/opencv/cvat/pull/1352>)
@ -152,10 +284,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Images without annotations now also included in dataset/annotations export (<https://github.com/opencv/cvat/issues/525>) - Images without annotations now also included in dataset/annotations export (<https://github.com/opencv/cvat/issues/525>)
### Removed ### Removed
- `annotation` application is replaced with `dataset_manager` (<https://github.com/opencv/cvat/pull/1352>) - `annotation` application is replaced with `dataset_manager` (<https://github.com/opencv/cvat/pull/1352>)
- `_DATUMARO_INIT_LOGLEVEL` env. variable is removed in favor of regular `--loglevel` cli parameter (<https://github.com/opencv/cvat/pull/1583>) - `_DATUMARO_INIT_LOGLEVEL` env. variable is removed in favor of regular `--loglevel` cli parameter (<https://github.com/opencv/cvat/pull/1583>)
### Fixed ### Fixed
- Categories for empty projects with no sources are taken from own dataset (<https://github.com/opencv/cvat/pull/1352>) - Categories for empty projects with no sources are taken from own dataset (<https://github.com/opencv/cvat/pull/1352>)
- Added directory removal on error during `extract` command (<https://github.com/opencv/cvat/pull/1352>) - Added directory removal on error during `extract` command (<https://github.com/opencv/cvat/pull/1352>)
- Added debug error message on incorrect XPath (<https://github.com/opencv/cvat/pull/1352>) - Added debug error message on incorrect XPath (<https://github.com/opencv/cvat/pull/1352>)
@ -173,7 +307,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed COCO keypoints skeleton parsing and saving (<https://github.com/opencv/cvat/issues/1539>) - Fixed COCO keypoints skeleton parsing and saving (<https://github.com/opencv/cvat/issues/1539>)
- `tf.placeholder() is not compatible with eager execution` exception for auto_segmentation (<https://github.com/opencv/cvat/pull/1562>) - `tf.placeholder() is not compatible with eager execution` exception for auto_segmentation (<https://github.com/opencv/cvat/pull/1562>)
- Canvas cannot be moved with move functionality on left mouse key (<https://github.com/opencv/cvat/pull/1573>) - Canvas cannot be moved with move functionality on left mouse key (<https://github.com/opencv/cvat/pull/1573>)
- Deep extreme cut request is sent when draw any shape with Make AI polygon option enabled (<https://github.com/opencv/cvat/pull/1573>) - Deep extreme cut request is sent when draw any shape with Make AI polygon option enabled (<https://github.com/opencv/cvat/pull/1573>)
- Fixed an error when exporting a task with cuboids to any format except CVAT (<https://github.com/opencv/cvat/pull/1577>) - Fixed an error when exporting a task with cuboids to any format except CVAT (<https://github.com/opencv/cvat/pull/1577>)
- Synchronization with remote git repo (<https://github.com/opencv/cvat/pull/1582>) - Synchronization with remote git repo (<https://github.com/opencv/cvat/pull/1582>)
- A problem with mask to polygons conversion when polygons are too small (<https://github.com/opencv/cvat/pull/1581>) - A problem with mask to polygons conversion when polygons are too small (<https://github.com/opencv/cvat/pull/1581>)
@ -181,19 +315,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed an issue with `z_order` having no effect on segmentations (<https://github.com/opencv/cvat/pull/1589>) - Fixed an issue with `z_order` having no effect on segmentations (<https://github.com/opencv/cvat/pull/1589>)
### Security ### Security
- Permission group whitelist check for analytics view (<https://github.com/opencv/cvat/pull/1608>) - Permission group whitelist check for analytics view (<https://github.com/opencv/cvat/pull/1608>)
## [1.0.0-beta.2] - 2020-04-30 ## [1.0.0-beta.2] - 2020-04-30
### Added ### Added
- Re-Identification algorithm to merging bounding boxes automatically to the new UI (<https://github.com/opencv/cvat/pull/1406>) - Re-Identification algorithm to merging bounding boxes automatically to the new UI (<https://github.com/opencv/cvat/pull/1406>)
- Methods ``import`` and ``export`` to import/export raw annotations for Job and Task in ``cvat-core`` (<https://github.com/opencv/cvat/pull/1406>) - Methods `import` and `export` to import/export raw annotations for Job and Task in `cvat-core` (<https://github.com/opencv/cvat/pull/1406>)
- Versioning of client packages (``cvat-core``, ``cvat-canvas``, ``cvat-ui``). Initial versions are set to 1.0.0 (<https://github.com/opencv/cvat/pull/1448>) - Versioning of client packages (`cvat-core`, `cvat-canvas`, `cvat-ui`). Initial versions are set to 1.0.0 (<https://github.com/opencv/cvat/pull/1448>)
- Cuboids feature was migrated from old UI to new one. (<https://github.com/opencv/cvat/pull/1451>) - Cuboids feature was migrated from old UI to new one. (<https://github.com/opencv/cvat/pull/1451>)
### Removed ### Removed
- Annotation convertation utils, currently supported natively via Datumaro framework (https://github.com/opencv/cvat/pull/1477) - Annotation convertation utils, currently supported natively via Datumaro framework (https://github.com/opencv/cvat/pull/1477)
### Fixed ### Fixed
- Auto annotation, TF annotation and Auto segmentation apps (https://github.com/opencv/cvat/pull/1409) - Auto annotation, TF annotation and Auto segmentation apps (https://github.com/opencv/cvat/pull/1409)
- Import works with truncated images now: "OSError:broken data stream" on corrupt images (https://github.com/opencv/cvat/pull/1430) - Import works with truncated images now: "OSError:broken data stream" on corrupt images (https://github.com/opencv/cvat/pull/1430)
- Hide functionality (H) doesn't work (<https://github.com/opencv/cvat/pull/1445>) - Hide functionality (H) doesn't work (<https://github.com/opencv/cvat/pull/1445>)
@ -210,8 +349,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Open task button doesn't work (https://github.com/opencv/cvat/pull/1474) - Open task button doesn't work (https://github.com/opencv/cvat/pull/1474)
## [1.0.0-beta.1] - 2020-04-15 ## [1.0.0-beta.1] - 2020-04-15
### Added ### Added
- Special behaviour for attribute value ``__undefined__`` (invisibility, no shortcuts to be set in AAM)
- Special behaviour for attribute value `__undefined__` (invisibility, no shortcuts to be set in AAM)
- Dialog window with some helpful information about using filters - Dialog window with some helpful information about using filters
- Ability to display a bitmap in the new UI - Ability to display a bitmap in the new UI
- Button to reset colors settings (brightness, saturation, contrast) in the new UI - Button to reset colors settings (brightness, saturation, contrast) in the new UI
@ -223,13 +364,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Deep extreme cut (semi-automatic segmentation) to the new UI (https://github.com/opencv/cvat/pull/1398) - Deep extreme cut (semi-automatic segmentation) to the new UI (https://github.com/opencv/cvat/pull/1398)
### Changed ### Changed
- Increase preview size of a task till 256, 256 on the server - Increase preview size of a task till 256, 256 on the server
- Public ssh-keys are displayed in a dedicated window instead of console when create a task with a repository - Public ssh-keys are displayed in a dedicated window instead of console when create a task with a repository
- React UI is the primary UI - React UI is the primary UI
### Fixed ### Fixed
- Cleaned up memory in Auto Annotation to enable long running tasks on videos - Cleaned up memory in Auto Annotation to enable long running tasks on videos
- New shape is added when press ``esc`` when drawing instead of cancellation - New shape is added when press `esc` when drawing instead of cancellation
- Dextr segmentation doesn't work. - Dextr segmentation doesn't work.
- `FileNotFoundError` during dump after moving format files - `FileNotFoundError` during dump after moving format files
- CVAT doesn't append outside shapes when merge polyshapes in old UI - CVAT doesn't append outside shapes when merge polyshapes in old UI
@ -254,23 +397,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Uploading annotations for tasks with multiple jobs (https://github.com/opencv/cvat/pull/1396) - Uploading annotations for tasks with multiple jobs (https://github.com/opencv/cvat/pull/1396)
## [1.0.0-alpha] - 2020-03-31 ## [1.0.0-alpha] - 2020-03-31
### Added ### Added
- Data streaming using chunks (https://github.com/opencv/cvat/pull/1007) - Data streaming using chunks (https://github.com/opencv/cvat/pull/1007)
- New UI: showing file names in UI (https://github.com/opencv/cvat/pull/1311) - New UI: showing file names in UI (https://github.com/opencv/cvat/pull/1311)
- New UI: delete a point from context menu (https://github.com/opencv/cvat/pull/1292) - New UI: delete a point from context menu (https://github.com/opencv/cvat/pull/1292)
### Fixed ### Fixed
- Git app cannot clone a repository (https://github.com/opencv/cvat/pull/1330) - Git app cannot clone a repository (https://github.com/opencv/cvat/pull/1330)
- New UI: preview position in task details (https://github.com/opencv/cvat/pull/1312) - New UI: preview position in task details (https://github.com/opencv/cvat/pull/1312)
- AWS deployment (https://github.com/opencv/cvat/pull/1316) - AWS deployment (https://github.com/opencv/cvat/pull/1316)
## [0.6.1] - 2020-03-21 ## [0.6.1] - 2020-03-21
### Changed ### Changed
- VOC task export now does not use official label map by default, but takes one - VOC task export now does not use official label map by default, but takes one
from the source task to avoid primary-class and class part name from the source task to avoid primary-class and class part name
clashing ([#1275](https://github.com/opencv/cvat/issues/1275)) clashing ([#1275](https://github.com/opencv/cvat/issues/1275))
### Fixed ### Fixed
- File names in LabelMe format export are no longer truncated ([#1259](https://github.com/opencv/cvat/issues/1259)) - File names in LabelMe format export are no longer truncated ([#1259](https://github.com/opencv/cvat/issues/1259))
- `occluded` and `z_order` annotation attributes are now correctly passed to Datumaro ([#1271](https://github.com/opencv/cvat/pull/1271)) - `occluded` and `z_order` annotation attributes are now correctly passed to Datumaro ([#1271](https://github.com/opencv/cvat/pull/1271))
- Annotation-less tasks now can be exported as empty datasets in COCO ([#1277](https://github.com/opencv/cvat/issues/1277)) - Annotation-less tasks now can be exported as empty datasets in COCO ([#1277](https://github.com/opencv/cvat/issues/1277))
@ -278,11 +427,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
allowed `frame_XXXXXX[.ext]` format ([#1274](https://github.com/opencv/cvat/pull/1274)) allowed `frame_XXXXXX[.ext]` format ([#1274](https://github.com/opencv/cvat/pull/1274))
### Security ### Security
- Bump acorn from 6.3.0 to 6.4.1 in /cvat-ui ([#1270](https://github.com/opencv/cvat/pull/1270)) - Bump acorn from 6.3.0 to 6.4.1 in /cvat-ui ([#1270](https://github.com/opencv/cvat/pull/1270))
## [0.6.0] - 2020-03-15 ## [0.6.0] - 2020-03-15
### Added ### Added
- Server only support for projects. Extend REST API v1 (/api/v1/projects*)
- Server only support for projects. Extend REST API v1 (/api/v1/projects\*)
- Ability to get basic information about users without admin permissions ([#750](https://github.com/opencv/cvat/issues/750)) - Ability to get basic information about users without admin permissions ([#750](https://github.com/opencv/cvat/issues/750))
- Changed REST API: removed PUT and added DELETE methods for /api/v1/users/ID - Changed REST API: removed PUT and added DELETE methods for /api/v1/users/ID
- Mask-RCNN Auto Annotation Script in OpenVINO format - Mask-RCNN Auto Annotation Script in OpenVINO format
@ -299,6 +451,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Git repositories can be specified with IPv4 address ([#827](https://github.com/opencv/cvat/pull/827)) - Git repositories can be specified with IPv4 address ([#827](https://github.com/opencv/cvat/pull/827))
### Changed ### Changed
- page_size parameter for all REST API methods - page_size parameter for all REST API methods
- React & Redux & Antd based dashboard - React & Redux & Antd based dashboard
- Yolov3 interpretation script fix and changes to mapping.json - Yolov3 interpretation script fix and changes to mapping.json
@ -306,6 +459,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added support for OpenVINO 2020 - Added support for OpenVINO 2020
### Fixed ### Fixed
- Exception in Git plugin [#826](https://github.com/opencv/cvat/issues/826) - Exception in Git plugin [#826](https://github.com/opencv/cvat/issues/826)
- Label ids in TFrecord format now start from 1 [#866](https://github.com/opencv/cvat/issues/866) - Label ids in TFrecord format now start from 1 [#866](https://github.com/opencv/cvat/issues/866)
- Mask problem in COCO JSON style [#718](https://github.com/opencv/cvat/issues/718) - Mask problem in COCO JSON style [#718](https://github.com/opencv/cvat/issues/718)
@ -314,15 +468,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Annotations can be filtered before dumping with Datumaro [#994](https://github.com/opencv/cvat/issues/994) - Annotations can be filtered before dumping with Datumaro [#994](https://github.com/opencv/cvat/issues/994)
## [0.5.2] - 2019-12-15 ## [0.5.2] - 2019-12-15
### Fixed ### Fixed
- Frozen version of scikit-image==0.15 in requirements.txt because next releases don't support Python 3.5 - Frozen version of scikit-image==0.15 in requirements.txt because next releases don't support Python 3.5
## [0.5.1] - 2019-10-17 ## [0.5.1] - 2019-10-17
### Added ### Added
- Integration with Zenodo.org (DOI) - Integration with Zenodo.org (DOI)
## [0.5.0] - 2019-09-12 ## [0.5.0] - 2019-09-12
### Added ### Added
- A converter to YOLO format - A converter to YOLO format
- Installation guide - Installation guide
- Linear interpolation for a single point - Linear interpolation for a single point
@ -341,13 +501,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added command line tool for performing common task operations (/utils/cli/) - Added command line tool for performing common task operations (/utils/cli/)
### Changed ### Changed
- Outside and keyframe buttons in the side panel for all interpolation shapes (they were only for boxes before) - Outside and keyframe buttons in the side panel for all interpolation shapes (they were only for boxes before)
- Improved error messages on the client side (#511) - Improved error messages on the client side (#511)
### Removed ### Removed
- "Flip images" has been removed. UI now contains rotation features. - "Flip images" has been removed. UI now contains rotation features.
### Fixed ### Fixed
- Incorrect width of shapes borders in some cases - Incorrect width of shapes borders in some cases
- Annotation parser for tracks with a start frame less than the first segment frame - Annotation parser for tracks with a start frame less than the first segment frame
- Interpolation on the server near outside frames - Interpolation on the server near outside frames
@ -365,43 +528,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Creating a video task with 0 overlap - Creating a video task with 0 overlap
### Security ### Security
- Upgraded Django, djangorestframework, and other packages - Upgraded Django, djangorestframework, and other packages
## [0.4.2] - 2019-06-03 ## [0.4.2] - 2019-06-03
### Fixed ### Fixed
- Fixed interaction with the server share in the auto annotation plugin - Fixed interaction with the server share in the auto annotation plugin
## [0.4.1] - 2019-05-14 ## [0.4.1] - 2019-05-14
### Fixed ### Fixed
- JavaScript syntax incompatibility with Google Chrome versions less than 72 - JavaScript syntax incompatibility with Google Chrome versions less than 72
## [0.4.0] - 2019-05-04 ## [0.4.0] - 2019-05-04
### Added ### Added
- OpenVINO auto annotation: it is possible to upload a custom model and annotate images automatically. - OpenVINO auto annotation: it is possible to upload a custom model and annotate images automatically.
- Ability to rotate images/video in the client part (Ctrl+R, Shift+Ctrl+R shortcuts) (#305) - Ability to rotate images/video in the client part (Ctrl+R, Shift+Ctrl+R shortcuts) (#305)
- The ReID application for automatic bounding box merging has been added (#299) - The ReID application for automatic bounding box merging has been added (#299)
- Keyboard shortcuts to switch next/previous default shape type (box, polygon etc) [Alt + <, Alt + >] (#316) - Keyboard shortcuts to switch next/previous default shape type (box, polygon etc) [Alt + <, Alt + >] (#316)
- Converter for VOC now supports interpolation tracks - Converter for VOC now supports interpolation tracks
- REST API (/api/v1/*, /api/docs) - REST API (/api/v1/\*, /api/docs)
- Semi-automatic semantic segmentation with the [Deep Extreme Cut](http://www.vision.ee.ethz.ch/~cvlsegmentation/dextr/) work - Semi-automatic semantic segmentation with the [Deep Extreme Cut](http://www.vision.ee.ethz.ch/~cvlsegmentation/dextr/) work
### Changed ### Changed
- Propagation setup has been moved from settings to bottom player panel - Propagation setup has been moved from settings to bottom player panel
- Additional events like "Debug Info" or "Fit Image" have been added for analitics - Additional events like "Debug Info" or "Fit Image" have been added for analitics
- Optional using LFS for git annotation storages (#314) - Optional using LFS for git annotation storages (#314)
### Deprecated ### Deprecated
- "Flip images" flag in the create task dialog will be removed. Rotation functionality in client part have been added instead. - "Flip images" flag in the create task dialog will be removed. Rotation functionality in client part have been added instead.
### Removed ### Removed
- -
### Fixed ### Fixed
- Django 2.1.5 (security fix, https://nvd.nist.gov/vuln/detail/CVE-2019-3498) - Django 2.1.5 (security fix, https://nvd.nist.gov/vuln/detail/CVE-2019-3498)
- Several scenarious which cause code 400 after undo/redo/save have been fixed (#315) - Several scenarious which cause code 400 after undo/redo/save have been fixed (#315)
## [0.3.0] - 2018-12-29 ## [0.3.0] - 2018-12-29
### Added ### Added
- Ability to copy Object URL and Frame URL via object context menu and player context menu respectively. - Ability to copy Object URL and Frame URL via object context menu and player context menu respectively.
- Ability to change opacity for selected shape with help "Selected Fill Opacity" slider. - Ability to change opacity for selected shape with help "Selected Fill Opacity" slider.
- Ability to remove polyshapes points by double click. - Ability to remove polyshapes points by double click.
@ -420,6 +596,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Buttons lock/hide for labels. They work for all objects with the same label on a current frame (#116) - Buttons lock/hide for labels. They work for all objects with the same label on a current frame (#116)
### Changed ### Changed
- Polyshape editing method has been improved. You can redraw part of shape instead of points cloning. - Polyshape editing method has been improved. You can redraw part of shape instead of points cloning.
- Unified shortcut (Esc) for close any mode instead of different shortcuts (Alt+N, Alt+G, Alt+M etc.). - Unified shortcut (Esc) for close any mode instead of different shortcuts (Alt+N, Alt+G, Alt+M etc.).
- Dump file contains information about data source (e.g. video name, archive name, ...) - Dump file contains information about data source (e.g. video name, archive name, ...)
@ -430,6 +607,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Drawing has become more convenience. Now it is possible to draw outside an image. Shapes will be automatically truncated after drawing process (#202) - Drawing has become more convenience. Now it is possible to draw outside an image. Shapes will be automatically truncated after drawing process (#202)
### Fixed ### Fixed
- Performance bottleneck has been fixed during you create new objects (draw, copy, merge etc). - Performance bottleneck has been fixed during you create new objects (draw, copy, merge etc).
- Label UI elements aren't updated after changelabel. - Label UI elements aren't updated after changelabel.
- Attribute annotation mode can use invalid shape position after resize or move shapes. - Attribute annotation mode can use invalid shape position after resize or move shapes.
@ -441,7 +619,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Text drawing outside of a frame in some cases (#202) - Text drawing outside of a frame in some cases (#202)
## [0.2.0] - 2018-09-28 ## [0.2.0] - 2018-09-28
### Added ### Added
- New annotation shapes: polygons, polylines, points - New annotation shapes: polygons, polylines, points
- Undo/redo feature - Undo/redo feature
- Grid to estimate size of objects - Grid to estimate size of objects
@ -459,42 +639,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Full screen view - Full screen view
### Changed ### Changed
- Documentation, screencasts, the primary screenshot - Documentation, screencasts, the primary screenshot
- Content-type for save_job request is application/json - Content-type for save_job request is application/json
### Fixed ### Fixed
- Player navigation if the browser's window is scrolled - Player navigation if the browser's window is scrolled
- Filter doesn't support dash (-) - Filter doesn't support dash (-)
- Several memory leaks - Several memory leaks
- Inconsistent extensions between filenames in an annotation file and real filenames - Inconsistent extensions between filenames in an annotation file and real filenames
## [0.1.2] - 2018-08-07 ## [0.1.2] - 2018-08-07
### Added ### Added
- 7z archive support when creating a task - 7z archive support when creating a task
- .vscode/launch.json file for developing with VS code - .vscode/launch.json file for developing with VS code
### Fixed ### Fixed
- #14: docker-compose down command as written in the readme does not remove volumes - #14: docker-compose down command as written in the readme does not remove volumes
- #15: all checkboxes in temporary attributes are checked when reopening job after saving the job - #15: all checkboxes in temporary attributes are checked when reopening job after saving the job
- #18: extend CONTRIBUTING.md - #18: extend CONTRIBUTING.md
- #19: using the same attribute for label twice -> stuck - #19: using the same attribute for label twice -> stuck
### Changed ### Changed
- More strict verification for labels with attributes - More strict verification for labels with attributes
## [0.1.1] - 2018-07-6 ## [0.1.1] - 2018-07-6
### Added ### Added
- Links on a screenshot, documentation, screencasts into README.md - Links on a screenshot, documentation, screencasts into README.md
- CONTRIBUTORS.md - CONTRIBUTORS.md
### Fixed ### Fixed
- GitHub documentation - GitHub documentation
## 0.1.0 - 2018-06-29 ## 0.1.0 - 2018-06-29
### Added ### Added
- Initial version - Initial version
## Template ## Template
``` ```
## [Unreleased] ## [Unreleased]
### Added ### Added

@ -10,78 +10,95 @@ patches and features.
## Development environment ## Development environment
Next steps should work on clear Ubuntu 18.04. - Install necessary dependencies:
- Install necessary dependencies: Ubuntu 18.04
```sh
sudo apt-get update && sudo apt-get --no-install-recommends install -y ffmpeg build-essential curl redis-server python3-dev python3-pip python3-venv python3-tk libldap2-dev libsasl2-dev
```
Also please make sure that you have installed ffmpeg with all necessary libav* libraries and pkg-config package.
```sh
# Node and npm (you can use default versions of these packages from apt (8.*, 3.*), but we would recommend to use newer versions)
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt-get install -y nodejs
# General dependencies
sudo apt-get install -y pkg-config
# Library components
sudo apt-get install -y \
libavformat-dev libavcodec-dev libavdevice-dev \
libavutil-dev libswscale-dev libswresample-dev libavfilter-dev
```
See [PyAV Dependencies installation guide](http://docs.mikeboers.com/pyav/develop/overview/installation.html#dependencies)
for details.
- Install [Visual Studio Code](https://code.visualstudio.com/docs/setup/linux#_debian-and-ubuntu-based-distributions)
for development
- Install CVAT on your local host:
```sh
git clone https://github.com/opencv/cvat
cd cvat && mkdir logs keys
python3 -m venv .env
. .env/bin/activate
pip install -U pip wheel setuptools
pip install -r cvat/requirements/development.txt
pip install -r datumaro/requirements.txt
python manage.py migrate
python manage.py collectstatic
```
- Create a super user for CVAT: ```sh
```sh sudo apt-get update && sudo apt-get --no-install-recommends install -y build-essential curl redis-server python3-dev python3-pip python3-venv python3-tk libldap2-dev libsasl2-dev pkg-config libavformat-dev libavcodec-dev libavdevice-dev libavutil-dev libswscale-dev libswresample-dev libavfilter-dev
$ python manage.py createsuperuser ```
Username (leave blank to use 'django'): ***
Email address: ***
Password: ***
Password (again): ***
```
- Install npm packages for UI and start UI debug server (run the following command from CVAT root directory): ```sh
```sh # Node and npm (you can use default versions of these packages from apt (8.*, 3.*), but we would recommend to use newer versions)
npm install && \ curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
cd cvat-core && npm install && \ sudo apt-get install -y nodejs
cd ../cvat-ui && npm install && npm start ```
```
- Open new terminal (Ctrl + Shift + T), run Visual Studio Code from the virtual environment MacOS 10.15
```sh
cd .. && source .env/bin/activate && code ```sh
``` brew install git python pyenv redis curl openssl node
```
- Install FFmpeg libraries (libav\*) version 4.0 or higher.
- Install [Visual Studio Code](https://code.visualstudio.com/docs/setup/linux#_debian-and-ubuntu-based-distributions)
for development
- Install CVAT on your local host:
```sh
git clone https://github.com/opencv/cvat
cd cvat && mkdir logs keys
python3 -m venv .env
. .env/bin/activate
pip install -U pip wheel setuptools
pip install -r cvat/requirements/development.txt
python manage.py migrate
python manage.py collectstatic
```
> Note for Mac users
>
> If you have any problems with installing dependencies from
> `cvat/requirements/*.txt`, you may need to reinstall your system python
> In some cases after system update it can be configured incorrectly and cannot compile some native modules
- Create a super user for CVAT:
```sh
$ python manage.py createsuperuser
Username (leave blank to use 'django'): ***
Email address: ***
Password: ***
Password (again): ***
```
- Install following VS Code extensions: - Install npm packages for UI and start UI debug server (run the following command from CVAT root directory):
- [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome)
- [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python)
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
- [Stylelint](https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint)
- [vscode-remark-lint](https://marketplace.visualstudio.com/items?itemName=drewbourne.vscode-remark-lint)
- [licenser](https://marketplace.visualstudio.com/items?itemName=ymotongpoo.licenser)
- [Trailing Spaces](https://marketplace.visualstudio.com/items?itemName=shardulm94.trailing-spaces)
- Reload Visual Studio Code from virtual environment ```sh
npm ci && \
cd cvat-core && npm ci && \
cd ../cvat-ui && npm ci && npm start
```
- Select `server: debug` configuration and start it (F5) to run REST server and its workers > Note for Mac users
>
> If you faced with error
>
> `Node Sass does not yet support your current environment: OS X 64-bit with Unsupported runtime (57)`
>
> Read this article [Node Sass does not yet support your current environment](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome)
- Open new terminal (Ctrl + Shift + T), run Visual Studio Code from the virtual environment
```sh
cd .. && source .env/bin/activate && code
```
- Install following VS Code extensions:
- [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome)
- [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python)
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
- [Stylelint](https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint)
- [vscode-remark-lint](https://marketplace.visualstudio.com/items?itemName=drewbourne.vscode-remark-lint)
- [licenser](https://marketplace.visualstudio.com/items?itemName=ymotongpoo.licenser)
- [Trailing Spaces](https://marketplace.visualstudio.com/items?itemName=shardulm94.trailing-spaces)
- Reload Visual Studio Code from virtual environment
- Select `server: debug` configuration and start it (F5) to run REST server and its workers
You have done! Now it is possible to insert breakpoints and debug server and client of the tool. You have done! Now it is possible to insert breakpoints and debug server and client of the tool.
@ -89,30 +106,32 @@ You have done! Now it is possible to insert breakpoints and debug server and cli
You develop CVAT under WSL (Windows subsystem for Linux) following next steps. You develop CVAT under WSL (Windows subsystem for Linux) following next steps.
- Install WSL using [this guide](https://docs.microsoft.com/ru-ru/windows/wsl/install-win10). - Install WSL using [this guide](https://docs.microsoft.com/ru-ru/windows/wsl/install-win10).
- Following this guide install Ubuntu 18.04 Linux distribution for WSL. - Following this guide install Ubuntu 18.04 Linux distribution for WSL.
- Run Ubuntu using start menu link or execute next command - Run Ubuntu using start menu link or execute next command
```powershell
wsl -d Ubuntu-18.04 ```powershell
``` wsl -d Ubuntu-18.04
```
- Run all commands from this isntallation guide in WSL Ubuntu shell.
- Run all commands from this isntallation guide in WSL Ubuntu shell.
## Setup additional components in development environment ## Setup additional components in development environment
### DL models as serverless functions ### DL models as serverless functions
Install [nuclio platform](https://github.com/nuclio/nuclio): Install [nuclio platform](https://github.com/nuclio/nuclio):
- You have to install `nuctl` command line tool to build and deploy serverless - You have to install `nuctl` command line tool to build and deploy serverless
functions. Download [the latest release]( functions. Download [the latest release](https://github.com/nuclio/nuclio/blob/development/docs/reference/nuctl/nuctl.md#download).
https://github.com/nuclio/nuclio/blob/development/docs/reference/nuctl/nuctl.md#download).
- The simplest way to explore Nuclio is to run its graphical user interface (GUI) - The simplest way to explore Nuclio is to run its graphical user interface (GUI)
of the Nuclio dashboard. All you need in order to run the dashboard is Docker. See of the Nuclio dashboard. All you need in order to run the dashboard is Docker. See
[nuclio documentation](https://github.com/nuclio/nuclio#quick-start-steps) [nuclio documentation](https://github.com/nuclio/nuclio#quick-start-steps)
for more details. for more details.
- Create `cvat` project inside nuclio dashboard where you will deploy new - Create `cvat` project inside nuclio dashboard where you will deploy new
serverless functions and deploy a couple of DL models. serverless functions and deploy a couple of DL models.
```bash ```bash
nuctl create project cvat nuctl create project cvat
@ -175,7 +194,7 @@ nuctl deploy --project-name cvat \
</details> </details>
- Display a list of running serverless functions using `nuctl` command or see them - Display a list of running serverless functions using `nuctl` command or see them
in nuclio dashboard: in nuclio dashboard:
```bash ```bash
nuctl get function nuctl get function
@ -192,7 +211,7 @@ nuctl get function
</details> </details>
- Test your deployed DL model as a serverless function. The command below - Test your deployed DL model as a serverless function. The command below
should work on Linux and Mac OS. should work on Linux and Mac OS.
```bash ```bash
image=$(curl https://upload.wikimedia.org/wikipedia/en/7/7d/Lenna_%28test_image%29.png --output - | base64 | tr -d '\n') image=$(curl https://upload.wikimedia.org/wikipedia/en/7/7d/Lenna_%28test_image%29.png --output - | base64 | tr -d '\n')
@ -233,6 +252,7 @@ Server = nuclio
] ]
``` ```
</details>
### Run Cypress tests ### Run Cypress tests
- Install Сypress as described in the [documentation](https://docs.cypress.io/guides/getting-started/installing-cypress.html). - Install Сypress as described in the [documentation](https://docs.cypress.io/guides/getting-started/installing-cypress.html).
- Run cypress tests: - Run cypress tests:
@ -252,12 +272,12 @@ little exception - we prefer 4 spaces for indentation of nested blocks and state
The project uses [a successful Git branching model](https://nvie.com/posts/a-successful-git-branching-model). The project uses [a successful Git branching model](https://nvie.com/posts/a-successful-git-branching-model).
Thus it has a couple of branches. Some of them are described below: Thus it has a couple of branches. Some of them are described below:
- `origin/master` to be the main branch where the source code of - `origin/master` to be the main branch where the source code of
HEAD always reflects a production-ready state HEAD always reflects a production-ready state
- `origin/develop` to be the main branch where the source code of - `origin/develop` to be the main branch where the source code of
HEAD always reflects a state with the latest delivered development HEAD always reflects a state with the latest delivered development
changes for the next release. Some would call this the “integration branch”. changes for the next release. Some would call this the “integration branch”.
## Using the issue tracker ## Using the issue tracker
@ -265,13 +285,14 @@ The issue tracker is the preferred channel for [bug reports](#bugs),
[features requests](#features) and [submitting pull [features requests](#features) and [submitting pull
requests](#pull-requests), but please respect the following restrictions: requests](#pull-requests), but please respect the following restrictions:
- Please **do not** use the issue tracker for personal support requests (use - Please **do not** use the issue tracker for personal support requests (use
[Stack Overflow](http://stackoverflow.com)). [Stack Overflow](http://stackoverflow.com)).
- Please **do not** derail or troll issues. Keep the discussion on topic and - Please **do not** derail or troll issues. Keep the discussion on topic and
respect the opinions of others. respect the opinions of others.
<a name="bugs"></a> <a name="bugs"></a>
## Bug reports ## Bug reports
A bug is a _demonstrable problem_ that is caused by the code in the repository. A bug is a _demonstrable problem_ that is caused by the code in the repository.
@ -280,10 +301,10 @@ Good bug reports are extremely helpful - thank you!
Guidelines for bug reports: Guidelines for bug reports:
1. **Use the GitHub issue search** &mdash; check if the issue has already been 1. **Use the GitHub issue search** &mdash; check if the issue has already been
reported. reported.
1. **Check if the issue has been fixed** &mdash; try to reproduce it using the 1. **Check if the issue has been fixed** &mdash; try to reproduce it using the
latest `develop` branch in the repository. latest `develop` branch in the repository.
1. **Isolate the problem** &mdash; ideally create a reduced test case. 1. **Isolate the problem** &mdash; ideally create a reduced test case.
@ -310,6 +331,7 @@ Example:
> merits). > merits).
<a name="features"></a> <a name="features"></a>
## Feature requests ## Feature requests
Feature requests are welcome. But take a moment to find out whether your idea Feature requests are welcome. But take a moment to find out whether your idea
@ -318,6 +340,7 @@ case to convince the project's developers of the merits of this feature. Please
provide as much detail and context as possible. provide as much detail and context as possible.
<a name="pull-requests"></a> <a name="pull-requests"></a>
## Pull requests ## Pull requests
Good pull requests - patches, improvements, new features - are a fantastic Good pull requests - patches, improvements, new features - are a fantastic
@ -362,10 +385,10 @@ project:
``` ```
1. Commit your changes in logical chunks. Please adhere to these [git commit 1. Commit your changes in logical chunks. Please adhere to these [git commit
message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
or your code is unlikely be merged into the main project. Use Git's or your code is unlikely be merged into the main project. Use Git's
[interactive rebase](https://docs.github.com/en/github/using-git/about-git-rebase) [interactive rebase](https://docs.github.com/en/github/using-git/about-git-rebase)
feature to tidy up your commits before making them public. feature to tidy up your commits before making them public.
1. Locally merge (or rebase) the upstream development branch into your topic branch: 1. Locally merge (or rebase) the upstream development branch into your topic branch:

@ -1,4 +1,53 @@
FROM ubuntu:16.04 FROM ubuntu:20.04 as build-image
ARG http_proxy
ARG https_proxy
ARG no_proxy
ARG socks_proxy
ARG DJANGO_CONFIGURATION
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \
apache2-dev \
build-essential \
curl \
libldap2-dev \
libsasl2-dev \
nasm \
git \
pkg-config \
python3-dev \
python3-pip \
python3-venv && \
rm -rf /var/lib/apt/lists/*
# Compile Openh264 and FFmpeg
ARG PREFIX=/opt/ffmpeg
ARG PKG_CONFIG_PATH=${PREFIX}/lib/pkgconfig
ENV FFMPEG_VERSION=4.3.1 \
OPENH264_VERSION=2.1.1
WORKDIR /tmp/openh264
RUN curl -sL https://github.com/cisco/openh264/archive/v${OPENH264_VERSION}.tar.gz --output openh264-${OPENH264_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f openh264-${OPENH264_VERSION}.tar.gz && \
make -j5 && make install PREFIX=${PREFIX} && make clean
WORKDIR /tmp/ffmpeg
RUN curl -sLO https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \
tar -jx --strip-components=1 -f ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \
./configure --disable-nonfree --disable-gpl --enable-libopenh264 --enable-shared --disable-static --prefix="${PREFIX}" && \
make -j5 && make install && make distclean
# Install requirements
RUN python3 -m venv /opt/venv
ENV PATH="/opt/venv/bin:${PATH}"
RUN python3 -m pip install --no-cache-dir -U pip==20.0.1 setuptools==49.6.0 wheel==0.35.1
COPY cvat/requirements/ /tmp/requirements/
RUN DATUMARO_HEADLESS=1 python3 -m pip install --no-cache-dir -r /tmp/requirements/${DJANGO_CONFIGURATION}.txt
FROM ubuntu:20.04
ARG http_proxy ARG http_proxy
ARG https_proxy ARG https_proxy
@ -21,68 +70,26 @@ ENV DJANGO_CONFIGURATION=${DJANGO_CONFIGURATION}
# Install necessary apt packages # Install necessary apt packages
RUN apt-get update && \ RUN apt-get update && \
apt-get --no-install-recommends install -yq \
software-properties-common && \
add-apt-repository ppa:mc3man/xerus-media -y && \
add-apt-repository ppa:mc3man/gstffmpeg-keep -y && \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \ DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \
apache2 \ apache2 \
apache2-dev \
apt-utils \
build-essential \
libapache2-mod-xsendfile \ libapache2-mod-xsendfile \
supervisor \ supervisor \
ffmpeg \ libldap-2.4-2 \
gstreamer0.10-ffmpeg \ libsasl2-2 \
libavcodec-dev \ libpython3-dev \
libavdevice-dev \
libavfilter-dev \
libavformat-dev \
libavutil-dev \
libswresample-dev \
libswscale-dev \
libldap2-dev \
libsasl2-dev \
pkg-config \
python3-dev \
python3-pip \
tzdata \ tzdata \
python3-distutils \
p7zip-full \ p7zip-full \
git \ git \
ssh \ git-lfs \
poppler-utils \ poppler-utils \
ssh \
curl && \ curl && \
curl https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash && \
apt-get --no-install-recommends install -y git-lfs && git lfs install && \
python3 -m pip install --no-cache-dir -U pip==20.0.1 setuptools==49.6.0 wheel==0.35.1 && \
ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && \ ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && \
dpkg-reconfigure -f noninteractive tzdata && \ dpkg-reconfigure -f noninteractive tzdata && \
add-apt-repository --remove ppa:mc3man/gstffmpeg-keep -y && \
add-apt-repository --remove ppa:mc3man/xerus-media -y && \
rm -rf /var/lib/apt/lists/* && \ rm -rf /var/lib/apt/lists/* && \
echo 'application/wasm wasm' >> /etc/mime.types echo 'application/wasm wasm' >> /etc/mime.types
# Add a non-root user
ENV USER=${USER}
ENV HOME /home/${USER}
WORKDIR ${HOME}
RUN adduser --shell /bin/bash --disabled-password --gecos "" ${USER} && \
if [ -z ${socks_proxy} ]; then \
echo export "GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=30\"" >> ${HOME}/.bashrc; \
else \
echo export "GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=30 -o ProxyCommand='nc -X 5 -x ${socks_proxy} %h %p'\"" >> ${HOME}/.bashrc; \
fi
COPY components /tmp/components
# Install and initialize CVAT, copy all necessary files
COPY cvat/requirements/ /tmp/requirements/
COPY supervisord.conf mod_wsgi.conf wait-for-it.sh manage.py ${HOME}/
RUN python3 -m pip install --no-cache-dir -r /tmp/requirements/${DJANGO_CONFIGURATION}.txt
# pycocotools package is impossible to install with its dependencies by one pip install command
RUN python3 -m pip install --no-cache-dir pycocotools==2.0.0
ARG CLAM_AV ARG CLAM_AV
ENV CLAM_AV=${CLAM_AV} ENV CLAM_AV=${CLAM_AV}
RUN if [ "$CLAM_AV" = "yes" ]; then \ RUN if [ "$CLAM_AV" = "yes" ]; then \
@ -96,23 +103,52 @@ RUN if [ "$CLAM_AV" = "yes" ]; then \
rm -rf /var/lib/apt/lists/*; \ rm -rf /var/lib/apt/lists/*; \
fi fi
COPY ssh ${HOME}/.ssh # Add a non-root user
COPY utils ${HOME}/utils ENV USER=${USER}
COPY cvat/ ${HOME}/cvat ENV HOME /home/${USER}
COPY cvat-core/ ${HOME}/cvat-core RUN adduser --shell /bin/bash --disabled-password --gecos "" ${USER} && \
COPY cvat-data/ ${HOME}/cvat-data if [ -z ${socks_proxy} ]; then \
COPY tests ${HOME}/tests echo export "GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=30\"" >> ${HOME}/.bashrc; \
COPY datumaro/ ${HOME}/datumaro else \
echo export "GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=30 -o ProxyCommand='nc -X 5 -x ${socks_proxy} %h %p'\"" >> ${HOME}/.bashrc; \
fi
RUN python3 -m pip install --no-cache-dir -r ${HOME}/datumaro/requirements.txt ARG INSTALL_SOURCES='no'
WORKDIR ${HOME}/sources
RUN if [ "$INSTALL_SOURCES" = "yes" ]; then \
sed -Ei 's/^# deb-src /deb-src /' /etc/apt/sources.list && \
apt-get update && \
dpkg --get-selections | while read -r line; do \
package=$(echo "$line" | awk '{print $1}'); \
mkdir "$package"; \
( \
cd "$package"; \
apt-get -q --download-only source "$package"; \
) \
done && \
rm -rf /var/lib/apt/lists/*; \
fi
COPY --from=build-image /tmp/openh264/openh264*.tar.gz /tmp/ffmpeg/ffmpeg*.tar.bz2 ${HOME}/sources/
RUN chown -R ${USER}:${USER} . # Copy python virtual enviroment and FFmpeg binaries from build-image
COPY --from=build-image /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:${PATH}"
COPY --from=build-image /opt/ffmpeg /usr
# Install and initialize CVAT, copy all necessary files
COPY --chown=${USER} components /tmp/components
COPY --chown=${USER} ssh ${HOME}/.ssh
COPY --chown=${USER} supervisord.conf mod_wsgi.conf wait-for-it.sh manage.py ${HOME}/
COPY --chown=${USER} cvat/ ${HOME}/cvat
COPY --chown=${USER} utils/ ${HOME}/utils
COPY --chown=${USER} tests/ ${HOME}/tests
# RUN all commands below as 'django' user # RUN all commands below as 'django' user
USER ${USER} USER ${USER}
WORKDIR ${HOME}
RUN mkdir data share media keys logs /tmp/supervisord RUN mkdir data share media keys logs /tmp/supervisord
RUN python3 manage.py collectstatic RUN python3 manage.py collectstatic
EXPOSE 8080 8443 EXPOSE 8080
ENTRYPOINT ["/usr/bin/supervisord"] ENTRYPOINT ["/usr/bin/supervisord"]

@ -3,24 +3,35 @@ FROM cvat/server
ENV DJANGO_CONFIGURATION=testing ENV DJANGO_CONFIGURATION=testing
USER root USER root
RUN curl https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \ RUN apt-get update && \
echo 'deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main' | tee /etc/apt/sources.list.d/google-chrome.list && \
curl https://deb.nodesource.com/setup_12.x | bash - && \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \ DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \
gpg-agent \
gnupg2 \
apt-utils \ apt-utils \
build-essential \ build-essential \
google-chrome-stable \
nodejs \
python3-dev \ python3-dev \
ruby \ ruby \
&& \ && \
curl https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \
echo 'deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main' | tee /etc/apt/sources.list.d/google-chrome.list && \
curl https://deb.nodesource.com/setup_12.x | bash - && \
DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \
google-chrome-stable \
nodejs \
&& \
rm -rf /var/lib/apt/lists/*; rm -rf /var/lib/apt/lists/*;
RUN python3 -m pip install --no-cache-dir -r /tmp/requirements/${DJANGO_CONFIGURATION}.txt && \ COPY cvat/requirements/ /tmp/requirements/
RUN DATUMARO_HEADLESS=1 python3 -m pip install --no-cache-dir -r /tmp/requirements/${DJANGO_CONFIGURATION}.txt && \
python3 -m pip install --no-cache-dir coveralls python3 -m pip install --no-cache-dir coveralls
RUN gem install coveralls-lcov RUN gem install coveralls-lcov
COPY utils ${HOME}/utils
COPY cvat-core ${HOME}/cvat-core
COPY cvat-data ${HOME}/cvat-data
COPY tests ${HOME}/tests
COPY .coveragerc . COPY .coveragerc .
ENTRYPOINT [] ENTRYPOINT []

@ -15,29 +15,29 @@ ENV TERM=xterm \
LANG='C.UTF-8' \ LANG='C.UTF-8' \
LC_ALL='C.UTF-8' LC_ALL='C.UTF-8'
RUN apk add python3 g++ make
# Install dependencies # Install dependencies
COPY cvat-core/package*.json /tmp/cvat-core/ COPY cvat-core/package*.json /tmp/cvat-core/
COPY cvat-canvas/package*.json /tmp/cvat-canvas/ COPY cvat-canvas/package*.json /tmp/cvat-canvas/
COPY cvat-ui/package*.json /tmp/cvat-ui/ COPY cvat-ui/package*.json /tmp/cvat-ui/
COPY cvat-data/package*.json /tmp/cvat-data/ COPY cvat-data/package*.json /tmp/cvat-data/
RUN npm config set loglevel info
# Install cvat-data dependencies # Install cvat-data dependencies
WORKDIR /tmp/cvat-data/ WORKDIR /tmp/cvat-data/
RUN npm install RUN npm ci
# Install cvat-core dependencies # Install cvat-core dependencies
WORKDIR /tmp/cvat-core/ WORKDIR /tmp/cvat-core/
RUN npm install RUN npm ci
# Install cvat-canvas dependencies # Install cvat-canvas dependencies
WORKDIR /tmp/cvat-canvas/ WORKDIR /tmp/cvat-canvas/
RUN npm install RUN npm ci
# Install cvat-ui dependencies # Install cvat-ui dependencies
WORKDIR /tmp/cvat-ui/ WORKDIR /tmp/cvat-ui/
RUN npm install RUN npm ci
# Build source code # Build source code
COPY cvat-data/ /tmp/cvat-data/ COPY cvat-data/ /tmp/cvat-data/

@ -1,9 +1,9 @@
# Computer Vision Annotation Tool (CVAT) # Computer Vision Annotation Tool (CVAT)
[![Build Status](https://travis-ci.org/opencv/cvat.svg?branch=develop)](https://travis-ci.org/opencv/cvat) [![Build Status](https://travis-ci.org/openvinotoolkit/cvat.svg?branch=develop)](https://travis-ci.org/openvinotoolkit/cvat)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/840351da141e4eaeac6476fd19ec0a33)](https://app.codacy.com/app/cvat/cvat?utm_source=github.com&utm_medium=referral&utm_content=opencv/cvat&utm_campaign=Badge_Grade_Dashboard) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/b9899c72f2764df0b5d26390cb872e21)](https://app.codacy.com/gh/openvinotoolkit/cvat?utm_source=github.com&utm_medium=referral&utm_content=openvinotoolkit/cvat&utm_campaign=Badge_Grade_Dashboard)
[![Gitter chat](https://badges.gitter.im/opencv-cvat/gitter.png)](https://gitter.im/opencv-cvat) [![Gitter chat](https://badges.gitter.im/opencv-cvat/gitter.png)](https://gitter.im/opencv-cvat)
[![Coverage Status](https://coveralls.io/repos/github/opencv/cvat/badge.svg?branch=)](https://coveralls.io/github/opencv/cvat?branch=develop) [![Coverage Status](https://coveralls.io/repos/github/openvinotoolkit/cvat/badge.svg?branch=develop)](https://coveralls.io/github/openvinotoolkit/cvat?branch=develop)
[![DOI](https://zenodo.org/badge/139156354.svg)](https://zenodo.org/badge/latestdoi/139156354) [![DOI](https://zenodo.org/badge/139156354.svg)](https://zenodo.org/badge/latestdoi/139156354)
CVAT is free, online, interactive video and image annotation CVAT is free, online, interactive video and image annotation
@ -19,7 +19,7 @@ annotation team. Try it online [cvat.org](https://cvat.org).
- [Installation guide](cvat/apps/documentation/installation.md) - [Installation guide](cvat/apps/documentation/installation.md)
- [User's guide](cvat/apps/documentation/user_guide.md) - [User's guide](cvat/apps/documentation/user_guide.md)
- [Django REST API documentation](#rest-api) - [Django REST API documentation](#rest-api)
- [Datumaro dataset framework](datumaro/README.md) - [Datumaro dataset framework](https://github.com/openvinotoolkit/datumaro/blob/develop/README.md)
- [Command line interface](utils/cli/) - [Command line interface](utils/cli/)
- [XML annotation format](cvat/apps/documentation/xml_format.md) - [XML annotation format](cvat/apps/documentation/xml_format.md)
- [AWS Deployment Guide](cvat/apps/documentation/AWS-Deployment-Guide.md) - [AWS Deployment Guide](cvat/apps/documentation/AWS-Deployment-Guide.md)
@ -32,7 +32,7 @@ annotation team. Try it online [cvat.org](https://cvat.org).
- [Annotation mode](https://youtu.be/vH_639N67HI) - [Annotation mode](https://youtu.be/vH_639N67HI)
- [Interpolation of bounding boxes](https://youtu.be/Hc3oudNuDsY) - [Interpolation of bounding boxes](https://youtu.be/Hc3oudNuDsY)
- [Interpolation of polygons](https://youtu.be/K4nis9lk92s) - [Interpolation of polygons](https://youtu.be/K4nis9lk92s)
- [Tag_annotation_video](https://youtu.be/62bI4mF-Xfk) - [Tag annotation video](https://youtu.be/62bI4mF-Xfk)
- [Attribute mode](https://youtu.be/iIkJsOkDzVA) - [Attribute mode](https://youtu.be/iIkJsOkDzVA)
- [Segmentation mode](https://youtu.be/9Fe_GzMLo3E) - [Segmentation mode](https://youtu.be/9Fe_GzMLo3E)
- [Tutorial for polygons](https://youtu.be/C7-r9lZbjBw) - [Tutorial for polygons](https://youtu.be/C7-r9lZbjBw)
@ -40,36 +40,41 @@ annotation team. Try it online [cvat.org](https://cvat.org).
## Supported annotation formats ## Supported annotation formats
Format selection is possible after clicking on the Upload annotation Format selection is possible after clicking on the Upload annotation and Dump
and Dump annotation buttons. [Datumaro](datumaro/README.md) dataset annotation buttons. [Datumaro](https://github.com/openvinotoolkit/datumaro)
framework allows additional dataset transformations dataset framework allows additional dataset transformations via its command
via its command line tool and Python library. line tool and Python library.
| Annotation format | Import | Export | For more information about supported formats look at the
| ------------------------------------------------------------------------------------------ | ------ | ------ | [documentation](cvat/apps/dataset_manager/formats/README.md#formats).
| [CVAT for images](cvat/apps/documentation/xml_format.md#annotation) | X | X |
| [CVAT for a video](cvat/apps/documentation/xml_format.md#interpolation) | X | X | | Annotation format | Import | Export |
| [Datumaro](datumaro/README.md) | | X | | ----------------------------------------------------------------------------- | ------ | ------ |
| [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X | | [CVAT for images](cvat/apps/documentation/xml_format.md#annotation) | X | X |
| Segmentation masks from [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X | | [CVAT for a video](cvat/apps/documentation/xml_format.md#interpolation) | X | X |
| [YOLO](https://pjreddie.com/darknet/yolo/) | X | X | | [Datumaro](https://github.com/openvinotoolkit/datumaro) | | X |
| [MS COCO Object Detection](http://cocodataset.org/#format-data) | X | X | | [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X |
| [TFrecord](https://www.tensorflow.org/tutorials/load_data/tf_records) | X | X | | Segmentation masks from [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X |
| [MOT](https://motchallenge.net/) | X | X | | [YOLO](https://pjreddie.com/darknet/yolo/) | X | X |
| [LabelMe 3.0](http://labelme.csail.mit.edu/Release3.0) | X | X | | [MS COCO Object Detection](http://cocodataset.org/#format-data) | X | X |
| [TFrecord](https://www.tensorflow.org/tutorials/load_data/tf_records) | X | X |
| [MOT](https://motchallenge.net/) | X | X |
| [LabelMe 3.0](http://labelme.csail.mit.edu/Release3.0) | X | X |
| [ImageNet](http://www.image-net.org) | X | X |
| [CamVid](http://mi.eng.cam.ac.uk/research/projects/VideoRec/CamVid/) | X | X |
## Deep learning models for automatic labeling ## Deep learning models for automatic labeling
| Name | Type | Framework | | Name | Type | Framework | CPU | GPU |
| ------------------------------------------------------------------------------------------------------- | ---------- | ---------- | | ------------------------------------------------------------------------------------------------------- | ---------- | ---------- | --- | --- |
| [Deep Extreme Cut](/serverless/openvino/dextr/nuclio) | interactor | OpenVINO | | [Deep Extreme Cut](/serverless/openvino/dextr/nuclio) | interactor | OpenVINO | X |
| [Faster RCNN](/serverless/tensorflow/faster_rcnn_inception_v2_coco/nuclio) | detector | TensorFlow | | [Faster RCNN](/serverless/tensorflow/faster_rcnn_inception_v2_coco/nuclio) | detector | TensorFlow | X | X |
| [Mask RCNN](/serverless/openvino/omz/public/mask_rcnn_inception_resnet_v2_atrous_coco/nuclio) | detector | OpenVINO | | [Mask RCNN](/serverless/openvino/omz/public/mask_rcnn_inception_resnet_v2_atrous_coco/nuclio) | detector | OpenVINO | X |
| [YOLO v3](/serverless/openvino/omz/public/yolo-v3-tf/nuclio) | detector | OpenVINO | | [YOLO v3](/serverless/openvino/omz/public/yolo-v3-tf/nuclio) | detector | OpenVINO | X |
| [Text detection v4](/serverless/openvino/omz/intel/text-detection-0004/nuclio) | detector | OpenVINO | | [Text detection v4](/serverless/openvino/omz/intel/text-detection-0004/nuclio) | detector | OpenVINO | X |
| [Semantic segmentation for ADAS](/serverless/openvino/omz/intel/semantic-segmentation-adas-0001/nuclio) | detector | OpenVINO | | [Semantic segmentation for ADAS](/serverless/openvino/omz/intel/semantic-segmentation-adas-0001/nuclio) | detector | OpenVINO | X |
| [Mask RCNN](/serverless/tensorflow/matterport/mask_rcnn/nuclio) | detector | TensorFlow | | [Mask RCNN](/serverless/tensorflow/matterport/mask_rcnn/nuclio) | detector | TensorFlow | X |
| [Object reidentification](/serverless/openvino/omz/intel/person-reidentification-retail-300/nuclio) | reid | OpenVINO | | [Object reidentification](/serverless/openvino/omz/intel/person-reidentification-retail-300/nuclio) | reid | OpenVINO | X |
## Online demo: [cvat.org](https://cvat.org) ## Online demo: [cvat.org](https://cvat.org)
@ -78,19 +83,21 @@ Try it online without local installation. Only own or assigned tasks
are visible to users. are visible to users.
Disabled features: Disabled features:
- [Analytics: management and monitoring of data annotation team](/components/analytics/README.md) - [Analytics: management and monitoring of data annotation team](/components/analytics/README.md)
Limitations: Limitations:
- No more than 10 tasks per user - No more than 10 tasks per user
- Uploaded data is limited to 500Mb - Uploaded data is limited to 500Mb
## REST API ## REST API
Automatically generated Swagger documentation for Django REST API is Automatically generated Swagger documentation for Django REST API is
available on ``<cvat_origin>/api/swagger`` available on `<cvat_origin>/api/swagger`
(default: ``localhost:8080/api/swagger``). (default: `localhost:8080/api/swagger`).
Swagger documentation is visiable on allowed hostes, Update environement variable in docker-compose.yml file with cvat hosted machine IP or domain name. Example - ``ALLOWED_HOSTS: 'localhost, 127.0.0.1'``) Swagger documentation is visiable on allowed hostes, Update environement variable in docker-compose.yml file with cvat hosted machine IP or domain name. Example - `ALLOWED_HOSTS: 'localhost, 127.0.0.1'`)
## LICENSE ## LICENSE
@ -104,16 +111,22 @@ contributors and other users.
However, if you have a feature request or a bug report that can reproduced, However, if you have a feature request or a bug report that can reproduced,
feel free to open an issue (with steps to reproduce the bug if it's a bug feel free to open an issue (with steps to reproduce the bug if it's a bug
report) on [GitHub* issues](https://github.com/opencv/cvat/issues). report) on [GitHub\* issues](https://github.com/opencv/cvat/issues).
If you are not sure or just want to browse other users common questions, If you are not sure or just want to browse other users common questions,
[Gitter chat](https://gitter.im/opencv-cvat) is the way to go. [Gitter chat](https://gitter.im/opencv-cvat) is the way to go.
Other ways to ask questions and get our support: Other ways to ask questions and get our support:
* [\#cvat](https://stackoverflow.com/search?q=%23cvat) tag on StackOverflow*
* [Forum on Intel Developer Zone](https://software.intel.com/en-us/forums/computer-vision) - [\#cvat](https://stackoverflow.com/search?q=%23cvat) tag on StackOverflow\*
- [Forum on Intel Developer Zone](https://software.intel.com/en-us/forums/computer-vision)
## Links ## Links
- [Intel AI blog: New Computer Vision Tool Accelerates Annotation of Digital Images and Video](https://www.intel.ai/introducing-cvat) - [Intel AI blog: New Computer Vision Tool Accelerates Annotation of Digital Images and Video](https://www.intel.ai/introducing-cvat)
- [Intel Software: Computer Vision Annotation Tool: A Universal Approach to Data Annotation](https://software.intel.com/en-us/articles/computer-vision-annotation-tool-a-universal-approach-to-data-annotation) - [Intel Software: Computer Vision Annotation Tool: A Universal Approach to Data Annotation](https://software.intel.com/en-us/articles/computer-vision-annotation-tool-a-universal-approach-to-data-annotation)
- [VentureBeat: Intel open-sources CVAT, a toolkit for data labeling](https://venturebeat.com/2019/03/05/intel-open-sources-cvat-a-toolkit-for-data-labeling/) - [VentureBeat: Intel open-sources CVAT, a toolkit for data labeling](https://venturebeat.com/2019/03/05/intel-open-sources-cvat-a-toolkit-for-data-labeling/)
## Projects using CVAT
- [Onepanel](https://github.com/onepanelio/core) - Onepanel is an open source vision AI platform that fully integrates CVAT with scalable data processing and parallelized training pipelines.

@ -5,12 +5,14 @@
It is possible to proxy annotation logs from client to ELK. To do that run the following command below: It is possible to proxy annotation logs from client to ELK. To do that run the following command below:
### Build docker image ### Build docker image
```bash ```bash
# From project root directory # From project root directory
docker-compose -f docker-compose.yml -f components/analytics/docker-compose.analytics.yml build docker-compose -f docker-compose.yml -f components/analytics/docker-compose.analytics.yml build
``` ```
### Run docker container ### Run docker container
```bash ```bash
# From project root directory # From project root directory
docker-compose -f docker-compose.yml -f components/analytics/docker-compose.analytics.yml up -d docker-compose -f docker-compose.yml -f components/analytics/docker-compose.analytics.yml up -d
@ -19,11 +21,12 @@ docker-compose -f docker-compose.yml -f components/analytics/docker-compose.anal
At the moment it is not possible to save advanced settings. Below values should be specified manually. At the moment it is not possible to save advanced settings. Below values should be specified manually.
## Time picker default ## Time picker default
{ {
"from": "now/d", "from": "now/d",
"to": "now/d", "to": "now/d",
"display": "Today", "display": "Today",
"section": 0 "section": 0
} }
## Time picker quick ranges ## Time picker quick ranges

@ -1,4 +1,4 @@
version: '2.3' version: '3.3'
services: services:
cvat_elasticsearch: cvat_elasticsearch:
container_name: cvat_elasticsearch container_name: cvat_elasticsearch
@ -35,9 +35,24 @@ services:
volumes: ['./components/analytics/kibana:/home/django/kibana:ro'] volumes: ['./components/analytics/kibana:/home/django/kibana:ro']
depends_on: ['cvat'] depends_on: ['cvat']
working_dir: '/home/django' working_dir: '/home/django'
entrypoint: ['bash', 'wait-for-it.sh', 'elasticsearch:9200', '-t', '0', '--', entrypoint:
'/bin/bash', 'wait-for-it.sh', 'kibana:5601', '-t', '0', '--', [
'/usr/bin/python3', 'kibana/setup.py', 'kibana/export.json'] 'bash',
'wait-for-it.sh',
'elasticsearch:9200',
'-t',
'0',
'--',
'/bin/bash',
'wait-for-it.sh',
'kibana:5601',
'-t',
'0',
'--',
'/usr/bin/python3',
'kibana/setup.py',
'kibana/export.json',
]
environment: environment:
no_proxy: elasticsearch,kibana,${no_proxy} no_proxy: elasticsearch,kibana,${no_proxy}
@ -52,7 +67,7 @@ services:
context: ./components/analytics/logstash context: ./components/analytics/logstash
args: args:
ELK_VERSION: 6.4.0 ELK_VERSION: 6.4.0
http_proxy: ${http_proxy} http_proxy: ${http_proxy}
https_proxy: ${https_proxy} https_proxy: ${https_proxy}
depends_on: ['cvat_elasticsearch'] depends_on: ['cvat_elasticsearch']
restart: always restart: always
@ -63,6 +78,7 @@ services:
DJANGO_LOG_SERVER_PORT: 5000 DJANGO_LOG_SERVER_PORT: 5000
DJANGO_LOG_VIEWER_HOST: kibana DJANGO_LOG_VIEWER_HOST: kibana
DJANGO_LOG_VIEWER_PORT: 5601 DJANGO_LOG_VIEWER_PORT: 5601
CVAT_ANALYTICS: 1
no_proxy: kibana,logstash,nuclio,${no_proxy} no_proxy: kibana,logstash,nuclio,${no_proxy}
volumes: volumes:

@ -1,3 +1,3 @@
http.host: 0.0.0.0 http.host: 0.0.0.0
script.painless.regex.enabled: true script.painless.regex.enabled: true
path.repo: ["/usr/share/elasticsearch/data/backup"] path.repo: ['/usr/share/elasticsearch/data/backup']

@ -40,19 +40,11 @@
"_type": "search", "_type": "search",
"_source": { "_source": {
"hits": 0, "hits": 0,
"sort": [ "sort": ["@timestamp", "desc"],
"@timestamp",
"desc"
],
"kibanaSavedObjectMeta": { "kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"index\":\"ec510550-c238-11e8-8e1b-758ef07f6de8\",\"highlightAll\":true,\"version\":true,\"query\":{\"language\":\"lucene\",\"query\":\"event:\\\"Send exception\\\"\"},\"filter\":[]}" "searchSourceJSON": "{\"index\":\"ec510550-c238-11e8-8e1b-758ef07f6de8\",\"highlightAll\":true,\"version\":true,\"query\":{\"language\":\"lucene\",\"query\":\"event:\\\"Send exception\\\"\"},\"filter\":[]}"
}, },
"columns": [ "columns": ["task", "type", "userid", "stack"],
"task",
"type",
"userid",
"stack"
],
"description": "", "description": "",
"title": "Table with exceptions", "title": "Table with exceptions",
"version": 1 "version": 1
@ -99,7 +91,7 @@
"_id": "ec510550-c238-11e8-8e1b-758ef07f6de8", "_id": "ec510550-c238-11e8-8e1b-758ef07f6de8",
"_type": "index-pattern", "_type": "index-pattern",
"_source": { "_source": {
"fields": "[{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@version.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"application\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"application.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"box count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"duration\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event\",\"type\":\"string\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"event.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"frame count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"object count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"points count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"polygon count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"polyline count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"task\",\"type\":\"string\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"task.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"timestamp\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"track count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"userid\",\"type\":\"string\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"userid.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"working_time\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", "fields": "[{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@version.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"application\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"application.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"box count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"duration\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event\",\"type\":\"string\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"event.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"frame count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"object count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"points count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"polygon count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"polyline count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"task\",\"type\":\"string\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"task\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"timestamp\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"track count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"userid\",\"type\":\"string\",\"count\":2,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"userid.keyword\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"working_time\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]",
"title": "cvat*", "title": "cvat*",
"timeFieldName": "@timestamp", "timeFieldName": "@timestamp",
"fieldFormatMap": "{\"duration\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"milliseconds\",\"outputFormat\":\"asSeconds\"}},\"working_time\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"milliseconds\",\"outputFormat\":\"asHours\"}}}" "fieldFormatMap": "{\"duration\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"milliseconds\",\"outputFormat\":\"asSeconds\"}},\"working_time\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"milliseconds\",\"outputFormat\":\"asHours\"}}}"
@ -164,7 +156,7 @@
"_type": "visualization", "_type": "visualization",
"_source": { "_source": {
"title": "List of tasks", "title": "List of tasks",
"visState": "{\"title\":\"List of tasks\",\"type\":\"table\",\"params\":{\"perPage\":20,\"showPartialRows\":false,\"showMetricsAtAllLevels\":false,\"sort\":{\"columnIndex\":2,\"direction\":\"desc\"},\"showTotal\":false,\"totalFunc\":\"sum\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"sum\",\"schema\":\"metric\",\"params\":{\"field\":\"working_time\",\"customLabel\":\"Working time (h)\"}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"task.keyword\",\"size\":1000,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"Task\"}},{\"id\":\"4\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"userid.keyword\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"_key\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"User\"}}]}", "visState": "{\"title\":\"List of tasks\",\"type\":\"table\",\"params\":{\"perPage\":20,\"showPartialRows\":false,\"showMetricsAtAllLevels\":false,\"sort\":{\"columnIndex\":2,\"direction\":\"desc\"},\"showTotal\":false,\"totalFunc\":\"sum\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"sum\",\"schema\":\"metric\",\"params\":{\"field\":\"working_time\",\"customLabel\":\"Working time (h)\"}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"task\",\"size\":1000,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"Task\"}},{\"id\":\"4\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"userid.keyword\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"_key\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"User\"}}]}",
"uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":2,\"direction\":\"desc\"}}}}", "uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":2,\"direction\":\"desc\"}}}}",
"description": "", "description": "",
"version": 1, "version": 1,

@ -1,5 +1,5 @@
server.host: 0.0.0.0 server.host: 0.0.0.0
elasticsearch.url: http://elasticsearch:9200 elasticsearch.url: http://elasticsearch:9200
elasticsearch.requestHeadersWhitelist: [ "cookie", "authorization", "x-forwarded-user" ] elasticsearch.requestHeadersWhitelist: ['cookie', 'authorization', 'x-forwarded-user']
kibana.defaultAppId: "discover" kibana.defaultAppId: 'discover'
server.basePath: /analytics server.basePath: /analytics

@ -0,0 +1,8 @@
## Serverless for Computer Vision Annotation Tool (CVAT)
### Run docker container
```bash
# From project root directory
docker-compose -f docker-compose.yml -f components/serverless/docker-compose.serverless.yml up -d
```

@ -0,0 +1,28 @@
version: '3.3'
services:
serverless:
container_name: nuclio
image: quay.io/nuclio/dashboard:1.5.8-amd64
restart: always
networks:
default:
aliases:
- nuclio
volumes:
- /tmp:/tmp
- /var/run/docker.sock:/var/run/docker.sock
environment:
http_proxy:
https_proxy:
no_proxy: 172.28.0.1,${no_proxy}
NUCLIO_CHECK_FUNCTION_CONTAINERS_HEALTHINESS: 'true'
ports:
- '8070:8070'
cvat:
environment:
CVAT_SERVERLESS: 1
no_proxy: kibana,logstash,nuclio,${no_proxy}
volumes:
cvat_events:

@ -0,0 +1 @@
webpack.config.js

@ -1,39 +1,34 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
module.exports = { module.exports = {
'env': { env: {
'node': true, node: true,
'browser': true,
'es6': true,
}, },
'parserOptions': { parserOptions: {
'parser': '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
'ecmaVersion': 6, ecmaVersion: 6,
}, },
'plugins': [ plugins: ['@typescript-eslint', 'import'],
'@typescript-eslint', extends: [
'import',
],
'extends': [
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'airbnb-typescript/base', 'airbnb-typescript/base',
'plugin:import/errors', 'plugin:import/errors',
'plugin:import/warnings', 'plugin:import/warnings',
'plugin:import/typescript', 'plugin:import/typescript',
], ],
'rules': { rules: {
'@typescript-eslint/no-explicit-any': 0, '@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/indent': ['warn', 4], '@typescript-eslint/indent': ['warn', 4],
'no-plusplus': 0, 'no-plusplus': 0,
'no-restricted-syntax': [ 'no-restricted-syntax': [
0, 0,
{ {
'selector': 'ForOfStatement' selector: 'ForOfStatement',
} },
], ],
'max-len': ['error', { code: 120 }],
'no-continue': 0, 'no-continue': 0,
'func-names': 0, 'func-names': 0,
'no-console': 0, // this rule deprecates console.log, console.warn etc. because 'it is not good in production code' 'no-console': 0, // this rule deprecates console.log, console.warn etc. because 'it is not good in production code'
@ -41,10 +36,10 @@ module.exports = {
'import/prefer-default-export': 0, // works incorrect with interfaces 'import/prefer-default-export': 0, // works incorrect with interfaces
'newline-per-chained-call': 0, // makes code uglier 'newline-per-chained-call': 0, // makes code uglier
}, },
'settings': { settings: {
'import/resolver': { 'import/resolver': {
'node': { node: {
'extensions': ['.ts', '.js', '.json'], extensions: ['.ts', '.js', '.json'],
}, },
}, },
}, },

@ -1,18 +1,21 @@
# Module CVAT-CANVAS # Module CVAT-CANVAS
## Description ## Description
The CVAT module written in TypeScript language. The CVAT module written in TypeScript language.
It presents a canvas to viewing, drawing and editing of annotations. It presents a canvas to viewing, drawing and editing of annotations.
## Versioning ## Versioning
If you make changes in this package, please do following: If you make changes in this package, please do following:
- After not important changes (typos, backward compatible bug fixes, refactoring) do: ``npm version patch`` - After not important changes (typos, backward compatible bug fixes, refactoring) do: `npm version patch`
- After changing API (backward compatible new features) do: ``npm version minor`` - After changing API (backward compatible new features) do: `npm version minor`
- After changing API (changes that break backward compatibility) do: ``npm version major`` - After changing API (changes that break backward compatibility) do: `npm version major`
## Commands ## Commands
- Building of the module from sources in the ```dist``` directory:
- Building of the module from sources in the `dist` directory:
```bash ```bash
npm run build npm run build
@ -22,6 +25,7 @@ npm run build -- --mode=development # without a minification
## Using ## Using
Canvas itself handles: Canvas itself handles:
- Shape context menu (PKM) - Shape context menu (PKM)
- Image moving (mousedrag) - Image moving (mousedrag)
- Image resizing (mousewheel) - Image resizing (mousewheel)
@ -51,6 +55,8 @@ Canvas itself handles:
MERGE = 'merge', MERGE = 'merge',
SPLIT = 'split', SPLIT = 'split',
GROUP = 'group', GROUP = 'group',
INTERACT = 'interact',
SELECT_ROI = 'select_roi',
DRAG_CANVAS = 'drag_canvas', DRAG_CANVAS = 'drag_canvas',
ZOOM_CANVAS = 'zoom_canvas', ZOOM_CANVAS = 'zoom_canvas',
} }
@ -70,6 +76,11 @@ Canvas itself handles:
crosshair?: boolean; crosshair?: boolean;
} }
interface InteractionData {
shapeType: string;
minVertices?: number;
}
interface GroupData { interface GroupData {
enabled: boolean; enabled: boolean;
resetGroup?: boolean; resetGroup?: boolean;
@ -83,6 +94,12 @@ Canvas itself handles:
enabled: boolean; enabled: boolean;
} }
interface InteractionResult {
points: number[];
shapeType: string;
button: number;
};
interface DrawnData { interface DrawnData {
shapeType: string; shapeType: string;
points: number[]; points: number[];
@ -95,14 +112,15 @@ Canvas itself handles:
interface Canvas { interface Canvas {
html(): HTMLDivElement; html(): HTMLDivElement;
setZLayer(zLayer: number | null): void; setup(frameData: any, objectStates: any[], zLayer?: number): void;
setup(frameData: any, objectStates: any[]): void; setupReviewROIs(reviewROIs: Record<number, number[]>): void;
activate(clientID: number, attributeID?: number): void; activate(clientID: number | null, attributeID?: number): void;
rotate(frameAngle: number): void; rotate(rotationAngle: number): void;
focus(clientID: number, padding?: number): void; focus(clientID: number, padding?: number): void;
fit(): void; fit(): void;
grid(stepX: number, stepY: number): void; grid(stepX: number, stepY: number): void;
interact(interactionData: InteractionData): void;
draw(drawData: DrawData): void; draw(drawData: DrawData): void;
group(groupData: GroupData): void; group(groupData: GroupData): void;
split(splitData: SplitData): void; split(splitData: SplitData): void;
@ -110,7 +128,8 @@ Canvas itself handles:
select(objectState: any): void; select(objectState: any): void;
fitCanvas(): void; fitCanvas(): void;
bitmap(enabled: boolean): void; bitmap(enable: boolean): void;
selectROI(enable: boolean): void;
dragCanvas(enable: boolean): void; dragCanvas(enable: boolean): void;
zoomCanvas(enable: boolean): void; zoomCanvas(enable: boolean): void;
@ -118,27 +137,33 @@ Canvas itself handles:
cancel(): void; cancel(): void;
configure(configuration: Configuration): void; configure(configuration: Configuration): void;
isAbleToChangeFrame(): boolean; isAbleToChangeFrame(): boolean;
readonly geometry: Geometry;
} }
``` ```
### API CSS ### API CSS
- All drawn objects (shapes, tracks) have an id ```cvat_canvas_shape_{objectState.clientID}``` - All drawn objects (shapes, tracks) have an id `cvat_canvas_shape_{objectState.clientID}`
- Drawn shapes and tracks have classes ```cvat_canvas_shape```, - Drawn shapes and tracks have classes `cvat_canvas_shape`,
```cvat_canvas_shape_activated```, `cvat_canvas_shape_activated`,
```cvat_canvas_shape_grouping```, `cvat_canvas_shape_grouping`,
```cvat_canvas_shape_merging```, `cvat_canvas_shape_merging`,
```cvat_canvas_shape_drawing```, `cvat_canvas_shape_drawing`,
```cvat_canvas_shape_occluded``` `cvat_canvas_shape_occluded`
- Drawn texts have the class ```cvat_canvas_text``` - Drawn review ROIs have an id `cvat_canvas_issue_region_{issue.id}`
- Tags have the class ```cvat_canvas_tag``` - Drawn review roi has the class `cvat_canvas_issue_region`
- Canvas image has ID ```cvat_canvas_image``` - Drawn texts have the class `cvat_canvas_text`
- Grid on the canvas has ID ```cvat_canvas_grid``` and ```cvat_canvas_grid_pattern``` - Tags have the class `cvat_canvas_tag`
- Crosshair during a draw has class ```cvat_canvas_crosshair``` - Canvas image has ID `cvat_canvas_image`
- Grid on the canvas has ID `cvat_canvas_grid` and `cvat_canvas_grid_pattern`
- Crosshair during a draw has class `cvat_canvas_crosshair`
- To stick something to a specific position you can use an element with id `cvat_canvas_attachment_board`
### Events ### Events
Standard JS events are used. Standard JS events are used.
```js ```js
- canvas.setup - canvas.setup
- canvas.activated => {state: ObjectState} - canvas.activated => {state: ObjectState}
@ -146,6 +171,7 @@ Standard JS events are used.
- canvas.moved => {states: ObjectState[], x: number, y: number} - canvas.moved => {states: ObjectState[], x: number, y: number}
- canvas.find => {states: ObjectState[], x: number, y: number} - canvas.find => {states: ObjectState[], x: number, y: number}
- canvas.drawn => {state: DrawnData} - canvas.drawn => {state: DrawnData}
- canvas.interacted => {shapes: InteractionResult[]}
- canvas.editstart - canvas.editstart
- canvas.edited => {state: ObjectState, points: number[]} - canvas.edited => {state: ObjectState, points: number[]}
- canvas.splitted => {state: ObjectState} - canvas.splitted => {state: ObjectState}
@ -159,53 +185,62 @@ Standard JS events are used.
- canvas.zoom - canvas.zoom
- canvas.fit - canvas.fit
- canvas.dragshape => {id: number} - canvas.dragshape => {id: number}
- canvas.roiselected => {points: number[]}
- canvas.resizeshape => {id: number} - canvas.resizeshape => {id: number}
- canvas.contextmenu => { mouseEvent: MouseEvent, objectState: ObjectState, pointID: number } - canvas.contextmenu => { mouseEvent: MouseEvent, objectState: ObjectState, pointID: number }
- canvas.error => { exception: Error }
``` ```
### WEB ### WEB
```js ```js
// Create an instance of a canvas // Create an instance of a canvas
const canvas = new window.canvas.Canvas(); const canvas = new window.canvas.Canvas();
console.log('Version ', window.canvas.CanvasVersion); console.log('Version ', window.canvas.CanvasVersion);
console.log('Current mode is ', window.canvas.mode()); console.log('Current mode is ', window.canvas.mode());
// Put canvas to a html container // Put canvas to a html container
htmlContainer.appendChild(canvas.html()); htmlContainer.appendChild(canvas.html());
canvas.fitCanvas(); canvas.fitCanvas();
// Next you can use its API methods. For example: // Next you can use its API methods. For example:
canvas.rotate(270); canvas.rotate(270);
canvas.draw({ canvas.draw({
enabled: true, enabled: true,
shapeType: 'rectangle', shapeType: 'rectangle',
crosshair: true, crosshair: true,
rectDrawingMethod: window.Canvas.RectDrawingMethod.CLASSIC, rectDrawingMethod: window.Canvas.RectDrawingMethod.CLASSIC,
}); });
``` ```
<!--lint disable maximum-line-length-->
## API Reaction ## API Reaction
| | IDLE | GROUP | SPLIT | DRAW | MERGE | EDIT | DRAG | RESIZE | ZOOM_CANVAS | DRAG_CANVAS | | | IDLE | GROUP | SPLIT | DRAW | MERGE | EDIT | DRAG | RESIZE | ZOOM_CANVAS | DRAG_CANVAS | INTERACT |
|--------------|------|-------|-------|------|-------|------|------|--------|-------------|-------------| | ----------------- | ---- | ----- | ----- | ---- | ----- | ---- | ---- | ------ | ----------- | ----------- | -------- |
| setup() | + | + | + | +/- | + | +/- | +/- | +/- | + | + | | setup() | + | + | + | +/- | + | +/- | +/- | +/- | + | + | + |
| activate() | + | - | - | - | - | - | - | - | - | - | | activate() | + | - | - | - | - | - | - | - | - | - | - |
| rotate() | + | + | + | + | + | + | + | + | + | + | | rotate() | + | + | + | + | + | + | + | + | + | + | + |
| focus() | + | + | + | + | + | + | + | + | + | + | | focus() | + | + | + | + | + | + | + | + | + | + | + |
| fit() | + | + | + | + | + | + | + | + | + | + | | fit() | + | + | + | + | + | + | + | + | + | + | + |
| grid() | + | + | + | + | + | + | + | + | + | + | | grid() | + | + | + | + | + | + | + | + | + | + | + |
| draw() | + | - | - | - | - | - | - | - | - | - | | draw() | + | - | - | + | - | - | - | - | - | - | - |
| split() | + | - | + | - | - | - | - | - | - | - | | interact() | + | - | - | - | - | - | - | - | - | - | + |
| group() | + | + | - | - | - | - | - | - | - | - | | split() | + | - | + | - | - | - | - | - | - | - | - |
| merge() | + | - | - | - | + | - | - | - | - | - | | group() | + | + | - | - | - | - | - | - | - | - | - |
| fitCanvas() | + | + | + | + | + | + | + | + | + | + | | merge() | + | - | - | - | + | - | - | - | - | - | - |
| dragCanvas() | + | - | - | - | - | - | + | - | - | + | | fitCanvas() | + | + | + | + | + | + | + | + | + | + | + |
| zoomCanvas() | + | - | - | - | - | - | - | + | + | - | | dragCanvas() | + | - | - | - | - | - | + | - | - | + | - |
| cancel() | - | + | + | + | + | + | + | + | + | + | | zoomCanvas() | + | - | - | - | - | - | - | + | + | - | - |
| configure() | + | + | + | + | + | + | + | + | + | + | | cancel() | - | + | + | + | + | + | + | + | + | + | + |
| bitmap() | + | + | + | + | + | + | + | + | + | + | | configure() | + | + | + | + | + | + | + | + | + | + | + |
| setZLayer() | + | + | + | + | + | + | + | + | + | + | | bitmap() | + | + | + | + | + | + | + | + | + | + | + |
| setZLayer() | + | + | + | + | + | + | + | + | + | + | + |
| setupReviewROIs() | + | + | + | + | + | + | + | + | + | + | + |
<!--lint enable maximum-line-length-->
You can call setup() during editing, dragging, and resizing only to update objects, not to change a frame. You can call setup() during editing, dragging, and resizing only to update objects, not to change a frame.
You can change frame during draw only when you do not redraw an existing object You can change frame during draw only when you do not redraw an existing object

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{ {
"name": "cvat-canvas", "name": "cvat-canvas",
"version": "2.0.2", "version": "2.2.1",
"description": "Part of Computer Vision Annotation Tool which presents its canvas library", "description": "Part of Computer Vision Annotation Tool which presents its canvas library",
"main": "src/canvas.ts", "main": "src/canvas.ts",
"scripts": { "scripts": {
@ -11,7 +11,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"svg.draggable.js": "2.2.2", "svg.draggable.js": "2.2.2",
"svg.draw.js": "^2.0.3", "svg.draw.js": "^2.0.4",
"svg.js": "2.7.1", "svg.js": "2.7.1",
"svg.resize.js": "1.4.3", "svg.resize.js": "1.4.3",
"svg.select.js": "3.0.1" "svg.select.js": "3.0.1"
@ -20,6 +20,7 @@
"@babel/cli": "^7.5.5", "@babel/cli": "^7.5.5",
"@babel/core": "^7.5.5", "@babel/core": "^7.5.5",
"@babel/plugin-proposal-class-properties": "^7.8.3", "@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-optional-chaining": "^7.11.0",
"@babel/preset-env": "^7.5.5", "@babel/preset-env": "^7.5.5",
"@babel/preset-typescript": "^7.3.3", "@babel/preset-typescript": "^7.3.3",
"@types/node": "^12.6.8", "@types/node": "^12.6.8",
@ -32,15 +33,15 @@
"eslint-config-airbnb-typescript": "^4.0.1", "eslint-config-airbnb-typescript": "^4.0.1",
"eslint-config-typescript-recommended": "^1.4.17", "eslint-config-typescript-recommended": "^1.4.17",
"eslint-plugin-import": "^2.18.2", "eslint-plugin-import": "^2.18.2",
"node-sass": "^4.13.1", "node-sass": "^4.14.1",
"nodemon": "^1.19.1", "nodemon": "^1.19.4",
"postcss-loader": "^3.0.0", "postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.7.0", "postcss-preset-env": "^6.7.0",
"sass-loader": "^8.0.2", "sass-loader": "^8.0.2",
"style-loader": "^1.0.0", "style-loader": "^1.0.0",
"typescript": "^3.5.3", "typescript": "^3.5.3",
"webpack": "^4.36.1", "webpack": "^4.44.2",
"webpack-cli": "^3.3.6", "webpack-cli": "^3.3.6",
"webpack-dev-server": "^3.7.2" "webpack-dev-server": "^3.11.0"
} }
} }

@ -2,7 +2,6 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
/* eslint-disable */
module.exports = { module.exports = {
parser: false, parser: false,
plugins: { plugins: {

@ -41,7 +41,7 @@ polyline.cvat_shape_drawing_opacity {
font-size: 1.2em; font-size: 1.2em;
fill: white; fill: white;
cursor: default; cursor: default;
font-family: Calibri, Candara, Segoe, "Segoe UI", Optima, Arial, sans-serif; font-family: Calibri, Candara, Segoe, 'Segoe UI', Optima, Arial, sans-serif;
text-shadow: 0 0 4px black; text-shadow: 0 0 4px black;
user-select: none; user-select: none;
pointer-events: none; pointer-events: none;
@ -58,6 +58,23 @@ polyline.cvat_shape_drawing_opacity {
fill: darkmagenta; fill: darkmagenta;
} }
.cvat_canvas_shape_region_selection {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
fill: white;
stroke: white;
}
.cvat_canvas_issue_region {
display: none;
stroke-width: 0;
}
circle.cvat_canvas_issue_region {
opacity: 1 !important;
}
polyline.cvat_canvas_shape_grouping { polyline.cvat_canvas_shape_grouping {
@extend .cvat_shape_action_dasharray; @extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity; @extend .cvat_shape_action_opacity;
@ -258,6 +275,15 @@ polyline.cvat_canvas_shape_splitting {
height: 100%; height: 100%;
} }
#cvat_canvas_attachment_board {
position: absolute;
z-index: 4;
pointer-events: none;
width: 100%;
height: 100%;
user-select: none;
}
@keyframes loadingAnimation { @keyframes loadingAnimation {
0% { 0% {
stroke-dashoffset: 1; stroke-dashoffset: 1;

@ -27,10 +27,16 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
private groups: SVGGElement[]; private groups: SVGGElement[];
private auxiliaryGroupID: number | null; private auxiliaryGroupID: number | null;
private auxiliaryClicks: number[]; private auxiliaryClicks: number[];
private listeners: Record<number, Record<number, { private listeners: Record<
click: (event: MouseEvent) => void; number,
dblclick: (event: MouseEvent) => void; Record<
}>>; number,
{
click: (event: MouseEvent) => void;
dblclick: (event: MouseEvent) => void;
}
>
>;
public constructor(frameContent: SVGSVGElement) { public constructor(frameContent: SVGSVGElement) {
this.frameContent = frameContent; this.frameContent = frameContent;
@ -47,12 +53,11 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
private removeMarkers(): void { private removeMarkers(): void {
this.groups.forEach((group: SVGGElement): void => { this.groups.forEach((group: SVGGElement): void => {
const groupID = group.dataset.groupId; const groupID = group.dataset.groupId;
Array.from(group.children) Array.from(group.children).forEach((circle: SVGCircleElement, pointID: number): void => {
.forEach((circle: SVGCircleElement, pointID: number): void => { circle.removeEventListener('click', this.listeners[+groupID][pointID].click);
circle.removeEventListener('click', this.listeners[+groupID][pointID].click); circle.removeEventListener('dblclick', this.listeners[+groupID][pointID].click);
circle.removeEventListener('dblclick', this.listeners[+groupID][pointID].click); circle.remove();
circle.remove(); });
});
group.remove(); group.remove();
}); });
@ -89,8 +94,9 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
if (this.auxiliaryGroupID !== null) { if (this.auxiliaryGroupID !== null) {
while (this.auxiliaryClicks.length > 0) { while (this.auxiliaryClicks.length > 0) {
const resetID = this.auxiliaryClicks.pop(); const resetID = this.auxiliaryClicks.pop();
this.groups[this.auxiliaryGroupID] this.groups[this.auxiliaryGroupID].children[resetID].classList.remove(
.children[resetID].classList.remove('cvat_canvas_autoborder_point_direction'); 'cvat_canvas_autoborder_point_direction',
);
} }
} }
@ -103,125 +109,125 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
private drawMarkers(transformedShapes: TransformedShape[]): void { private drawMarkers(transformedShapes: TransformedShape[]): void {
const svgNamespace = 'http://www.w3.org/2000/svg'; const svgNamespace = 'http://www.w3.org/2000/svg';
this.groups = transformedShapes this.groups = transformedShapes.map(
.map((shape: TransformedShape, groupID: number): SVGGElement => { (shape: TransformedShape, groupID: number): SVGGElement => {
const group = document.createElementNS(svgNamespace, 'g'); const group = document.createElementNS(svgNamespace, 'g');
group.setAttribute('data-group-id', `${groupID}`); group.setAttribute('data-group-id', `${groupID}`);
this.listeners[groupID] = this.listeners[groupID] || {}; this.listeners[groupID] = this.listeners[groupID] || {};
const circles = shape.points.split(/\s/).map(( const circles = shape.points.split(/\s/).map(
point: string, pointID: number, points: string[], (point: string, pointID: number, points: string[]): SVGCircleElement => {
): SVGCircleElement => { const [x, y] = point.split(',');
const [x, y] = point.split(',');
const circle = document.createElementNS(svgNamespace, 'circle');
const circle = document.createElementNS(svgNamespace, 'circle'); circle.classList.add('cvat_canvas_autoborder_point');
circle.classList.add('cvat_canvas_autoborder_point'); circle.setAttribute('fill', shape.color);
circle.setAttribute('fill', shape.color); circle.setAttribute('stroke', 'black');
circle.setAttribute('stroke', 'black'); circle.setAttribute('stroke-width', `${consts.POINTS_STROKE_WIDTH / this.scale}`);
circle.setAttribute('stroke-width', `${consts.POINTS_STROKE_WIDTH / this.scale}`); circle.setAttribute('cx', x);
circle.setAttribute('cx', x); circle.setAttribute('cy', y);
circle.setAttribute('cy', y); circle.setAttribute('r', `${consts.BASE_POINT_SIZE / this.scale}`);
circle.setAttribute('r', `${consts.BASE_POINT_SIZE / this.scale}`);
const click = (event: MouseEvent): void => {
const click = (event: MouseEvent): void => { event.stopPropagation();
event.stopPropagation();
// another shape was clicked
// another shape was clicked if (this.auxiliaryGroupID !== null && this.auxiliaryGroupID !== groupID) {
if (this.auxiliaryGroupID !== null this.resetAuxiliaryShape();
&& this.auxiliaryGroupID !== groupID
) {
this.resetAuxiliaryShape();
}
this.auxiliaryGroupID = groupID;
// up clicked group for convenience
this.frameContent.appendChild(group);
if (this.auxiliaryClicks[1] === pointID) {
// the second point was clicked twice
this.addPointToCurrentShape(+x, +y);
this.resetAuxiliaryShape();
return;
}
// the first point can not be clicked twice
// just ignore such a click if it is
if (this.auxiliaryClicks[0] !== pointID) {
this.auxiliaryClicks.push(pointID);
} else {
return;
}
// it is the first click
if (this.auxiliaryClicks.length === 1) {
const handler = this.currentShape.remember('_paintHandler');
// draw and remove initial point just to initialize data structures
if (!handler || !handler.startPoint) {
(this.currentShape as any).draw('point', event);
(this.currentShape as any).draw('undo');
} }
this.addPointToCurrentShape(+x, +y); this.auxiliaryGroupID = groupID;
// is is the second click // up clicked group for convenience
} else if (this.auxiliaryClicks.length === 2) { this.frameContent.appendChild(group);
circle.classList.add('cvat_canvas_autoborder_point_direction');
// it is the third click
} else {
// sign defines bypass direction
const landmarks = this.auxiliaryClicks;
const sign = Math.sign(landmarks[2] - landmarks[0])
* Math.sign(landmarks[1] - landmarks[0])
* Math.sign(landmarks[2] - landmarks[1]);
// go via a polygon and get vertexes
// the first vertex has been already drawn
const way = [];
for (let i = landmarks[0] + sign; ; i += sign) {
if (i < 0) {
i = points.length - 1;
} else if (i === points.length) {
i = 0;
}
way.push(points[i]); if (this.auxiliaryClicks[1] === pointID) {
// the second point was clicked twice
if (i === this.auxiliaryClicks[this.auxiliaryClicks.length - 1]) { this.addPointToCurrentShape(+x, +y);
// put the last element twice this.resetAuxiliaryShape();
// specific of svg.draw.js return;
// way.push(points[i]);
break;
}
} }
// remove the latest cursor position from drawing array // the first point can not be clicked twice
for (const wayPoint of way) { // just ignore such a click if it is
const [_x, _y] = wayPoint.split(',') if (this.auxiliaryClicks[0] !== pointID) {
.map((coordinate: string): number => +coordinate); this.auxiliaryClicks.push(pointID);
this.addPointToCurrentShape(_x, _y); } else {
return;
} }
this.resetAuxiliaryShape(); // it is the first click
} if (this.auxiliaryClicks.length === 1) {
}; const handler = this.currentShape.remember('_paintHandler');
// draw and remove initial point just to initialize data structures
if (!handler || !handler.startPoint) {
(this.currentShape as any).draw('point', event);
(this.currentShape as any).draw('undo');
}
this.addPointToCurrentShape(+x, +y);
// is is the second click
} else if (this.auxiliaryClicks.length === 2) {
circle.classList.add('cvat_canvas_autoborder_point_direction');
// it is the third click
} else {
// sign defines bypass direction
const landmarks = this.auxiliaryClicks;
const sign =
Math.sign(landmarks[2] - landmarks[0]) *
Math.sign(landmarks[1] - landmarks[0]) *
Math.sign(landmarks[2] - landmarks[1]);
// go via a polygon and get vertexes
// the first vertex has been already drawn
const way = [];
for (let i = landmarks[0] + sign; ; i += sign) {
if (i < 0) {
i = points.length - 1;
} else if (i === points.length) {
i = 0;
}
way.push(points[i]);
if (i === this.auxiliaryClicks[this.auxiliaryClicks.length - 1]) {
// put the last element twice
// specific of svg.draw.js
// way.push(points[i]);
break;
}
}
const dblclick = (event: MouseEvent): void => { // remove the latest cursor position from drawing array
event.stopPropagation(); for (const wayPoint of way) {
}; const [_x, _y] = wayPoint
.split(',')
.map((coordinate: string): number => +coordinate);
this.addPointToCurrentShape(_x, _y);
}
this.listeners[groupID][pointID] = { this.resetAuxiliaryShape();
click, }
dblclick, };
};
circle.addEventListener('mousedown', this.listeners[groupID][pointID].click); const dblclick = (event: MouseEvent): void => {
circle.addEventListener('dblclick', this.listeners[groupID][pointID].click); event.stopPropagation();
return circle; };
});
this.listeners[groupID][pointID] = {
click,
dblclick,
};
circle.addEventListener('mousedown', this.listeners[groupID][pointID].click);
circle.addEventListener('dblclick', this.listeners[groupID][pointID].click);
return circle;
},
);
group.append(...circles); group.append(...circles);
return group; return group;
}); },
);
this.frameContent.append(...this.groups); this.frameContent.append(...this.groups);
} }
@ -231,55 +237,54 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
this.removeMarkers(); this.removeMarkers();
const currentClientID = this.currentShape.node.dataset.originClientId; const currentClientID = this.currentShape.node.dataset.originClientId;
const shapes = Array.from(this.frameContent.getElementsByClassName('cvat_canvas_shape')) const shapes = Array.from(this.frameContent.getElementsByClassName('cvat_canvas_shape')).filter(
.filter((shape: HTMLElement): boolean => +shape.getAttribute('clientID') !== this.currentID); (shape: HTMLElement): boolean => +shape.getAttribute('clientID') !== this.currentID,
const transformedShapes = shapes.map((shape: HTMLElement): TransformedShape | null => { );
const color = shape.getAttribute('fill'); const transformedShapes = shapes
const clientID = shape.getAttribute('clientID'); .map((shape: HTMLElement): TransformedShape | null => {
const color = shape.getAttribute('fill');
if (color === null || clientID === null) return null; const clientID = shape.getAttribute('clientID');
if (+clientID === +currentClientID) {
return null; if (color === null || clientID === null) return null;
} if (+clientID === +currentClientID) {
let points = '';
if (shape.tagName === 'polyline' || shape.tagName === 'polygon') {
points = shape.getAttribute('points');
} else if (shape.tagName === 'rect') {
const x = +shape.getAttribute('x');
const y = +shape.getAttribute('y');
const width = +shape.getAttribute('width');
const height = +shape.getAttribute('height');
if (Number.isNaN(x) || Number.isNaN(y) || Number.isNaN(x) || Number.isNaN(x)) {
return null; return null;
} }
points = `${x},${y} ${x + width},${y} ${x + width},${y + height} ${x},${y + height}`; let points = '';
} else if (shape.tagName === 'g') { if (shape.tagName === 'polyline' || shape.tagName === 'polygon') {
const polylineID = shape.dataset.polylineId; points = shape.getAttribute('points');
const polyline = this.frameContent.getElementById(polylineID); } else if (shape.tagName === 'rect') {
if (polyline && polyline.getAttribute('points')) { const x = +shape.getAttribute('x');
points = polyline.getAttribute('points'); const y = +shape.getAttribute('y');
} else { const width = +shape.getAttribute('width');
return null; const height = +shape.getAttribute('height');
if (Number.isNaN(x) || Number.isNaN(y) || Number.isNaN(x) || Number.isNaN(x)) {
return null;
}
points = `${x},${y} ${x + width},${y} ${x + width},${y + height} ${x},${y + height}`;
} else if (shape.tagName === 'g') {
const polylineID = shape.dataset.polylineId;
const polyline = this.frameContent.getElementById(polylineID);
if (polyline && polyline.getAttribute('points')) {
points = polyline.getAttribute('points');
} else {
return null;
}
} }
}
return { return {
color, color,
points: points.trim(), points: points.trim(),
}; };
}).filter((state: TransformedShape | null): boolean => state !== null); })
.filter((state: TransformedShape | null): boolean => state !== null);
this.drawMarkers(transformedShapes); this.drawMarkers(transformedShapes);
} }
public autoborder( public autoborder(enabled: boolean, currentShape?: SVG.Shape, currentID?: number): void {
enabled: boolean,
currentShape?: SVG.Shape,
currentID?: number,
): void {
if (enabled && !this.enabled && currentShape) { if (enabled && !this.enabled && currentShape) {
this.enabled = true; this.enabled = true;
this.currentShape = currentShape; this.currentShape = currentShape;

@ -8,26 +8,18 @@ import {
MergeData, MergeData,
SplitData, SplitData,
GroupData, GroupData,
InteractionData as _InteractionData,
InteractionResult as _InteractionResult,
CanvasModel, CanvasModel,
CanvasModelImpl, CanvasModelImpl,
RectDrawingMethod, RectDrawingMethod,
CuboidDrawingMethod, CuboidDrawingMethod,
Configuration, Configuration,
Geometry,
} from './canvasModel'; } from './canvasModel';
import { Master } from './master';
import { import { CanvasController, CanvasControllerImpl } from './canvasController';
Master, import { CanvasView, CanvasViewImpl } from './canvasView';
} from './master';
import {
CanvasController,
CanvasControllerImpl,
} from './canvasController';
import {
CanvasView,
CanvasViewImpl,
} from './canvasView';
import '../scss/canvas.scss'; import '../scss/canvas.scss';
import pjson from '../../package.json'; import pjson from '../../package.json';
@ -37,12 +29,14 @@ const CanvasVersion = pjson.version;
interface Canvas { interface Canvas {
html(): HTMLDivElement; html(): HTMLDivElement;
setup(frameData: any, objectStates: any[], zLayer?: number): void; setup(frameData: any, objectStates: any[], zLayer?: number): void;
setupIssueRegions(issueRegions: Record<number, number[]>): void;
activate(clientID: number | null, attributeID?: number): void; activate(clientID: number | null, attributeID?: number): void;
rotate(rotationAngle: number): void; rotate(rotationAngle: number): void;
focus(clientID: number, padding?: number): void; focus(clientID: number, padding?: number): void;
fit(): void; fit(): void;
grid(stepX: number, stepY: number): void; grid(stepX: number, stepY: number): void;
interact(interactionData: InteractionData): void;
draw(drawData: DrawData): void; draw(drawData: DrawData): void;
group(groupData: GroupData): void; group(groupData: GroupData): void;
split(splitData: SplitData): void; split(splitData: SplitData): void;
@ -51,6 +45,7 @@ interface Canvas {
fitCanvas(): void; fitCanvas(): void;
bitmap(enable: boolean): void; bitmap(enable: boolean): void;
selectRegion(enable: boolean): void;
dragCanvas(enable: boolean): void; dragCanvas(enable: boolean): void;
zoomCanvas(enable: boolean): void; zoomCanvas(enable: boolean): void;
@ -58,6 +53,8 @@ interface Canvas {
cancel(): void; cancel(): void;
configure(configuration: Configuration): void; configure(configuration: Configuration): void;
isAbleToChangeFrame(): boolean; isAbleToChangeFrame(): boolean;
readonly geometry: Geometry;
} }
class CanvasImpl implements Canvas { class CanvasImpl implements Canvas {
@ -79,17 +76,22 @@ class CanvasImpl implements Canvas {
this.model.setup(frameData, objectStates, zLayer); this.model.setup(frameData, objectStates, zLayer);
} }
public setupIssueRegions(issueRegions: Record<number, number[]>): void {
this.model.setupIssueRegions(issueRegions);
}
public fitCanvas(): void { public fitCanvas(): void {
this.model.fitCanvas( this.model.fitCanvas(this.view.html().clientWidth, this.view.html().clientHeight);
this.view.html().clientWidth,
this.view.html().clientHeight,
);
} }
public bitmap(enable: boolean): void { public bitmap(enable: boolean): void {
this.model.bitmap(enable); this.model.bitmap(enable);
} }
public selectRegion(enable: boolean): void {
this.model.selectRegion(enable);
}
public dragCanvas(enable: boolean): void { public dragCanvas(enable: boolean): void {
this.model.dragCanvas(enable); this.model.dragCanvas(enable);
} }
@ -118,6 +120,10 @@ class CanvasImpl implements Canvas {
this.model.grid(stepX, stepY); this.model.grid(stepX, stepY);
} }
public interact(interactionData: InteractionData): void {
this.model.interact(interactionData);
}
public draw(drawData: DrawData): void { public draw(drawData: DrawData): void {
this.model.draw(drawData); this.model.draw(drawData);
} }
@ -153,13 +159,15 @@ class CanvasImpl implements Canvas {
public isAbleToChangeFrame(): boolean { public isAbleToChangeFrame(): boolean {
return this.model.isAbleToChangeFrame(); return this.model.isAbleToChangeFrame();
} }
public get geometry(): Geometry {
return this.model.geometry;
}
} }
export type InteractionData = _InteractionData;
export type InteractionResult = _InteractionResult;
export { export {
CanvasImpl as Canvas, CanvasImpl as Canvas, CanvasVersion, RectDrawingMethod, CuboidDrawingMethod, Mode as CanvasMode,
CanvasVersion,
Configuration,
RectDrawingMethod,
CuboidDrawingMethod,
Mode as CanvasMode,
}; };

@ -13,26 +13,33 @@ import {
SplitData, SplitData,
GroupData, GroupData,
Mode, Mode,
InteractionData,
Configuration,
} from './canvasModel'; } from './canvasModel';
export interface CanvasController { export interface CanvasController {
readonly objects: any[]; readonly objects: any[];
readonly issueRegions: Record<number, number[]>;
readonly zLayer: number | null; readonly zLayer: number | null;
readonly focusData: FocusData; readonly focusData: FocusData;
readonly activeElement: ActiveElement; readonly activeElement: ActiveElement;
readonly drawData: DrawData; readonly drawData: DrawData;
readonly interactionData: InteractionData;
readonly mergeData: MergeData; readonly mergeData: MergeData;
readonly splitData: SplitData; readonly splitData: SplitData;
readonly groupData: GroupData; readonly groupData: GroupData;
readonly selected: any; readonly selected: any;
readonly configuration: Configuration;
mode: Mode; mode: Mode;
geometry: Geometry; geometry: Geometry;
zoom(x: number, y: number, direction: number): void; zoom(x: number, y: number, direction: number): void;
draw(drawData: DrawData): void; draw(drawData: DrawData): void;
interact(interactionData: InteractionData): void;
merge(mergeData: MergeData): void; merge(mergeData: MergeData): void;
split(splitData: SplitData): void; split(splitData: SplitData): void;
group(groupData: GroupData): void; group(groupData: GroupData): void;
selectRegion(enabled: boolean): void;
enableDrag(x: number, y: number): void; enableDrag(x: number, y: number): void;
drag(x: number, y: number): void; drag(x: number, y: number): void;
disableDrag(): void; disableDrag(): void;
@ -84,6 +91,10 @@ export class CanvasControllerImpl implements CanvasController {
this.model.draw(drawData); this.model.draw(drawData);
} }
public interact(interactionData: InteractionData): void {
this.model.interact(interactionData);
}
public merge(mergeData: MergeData): void { public merge(mergeData: MergeData): void {
this.model.merge(mergeData); this.model.merge(mergeData);
} }
@ -96,6 +107,10 @@ export class CanvasControllerImpl implements CanvasController {
this.model.group(groupData); this.model.group(groupData);
} }
public selectRegion(enable: boolean): void {
this.model.selectRegion(enable);
}
public get geometry(): Geometry { public get geometry(): Geometry {
return this.model.geometry; return this.model.geometry;
} }
@ -108,6 +123,10 @@ export class CanvasControllerImpl implements CanvasController {
return this.model.zLayer; return this.model.zLayer;
} }
public get issueRegions(): Record<number, number[]> {
return this.model.issueRegions;
}
public get objects(): any[] { public get objects(): any[] {
return this.model.objects; return this.model.objects;
} }
@ -124,6 +143,10 @@ export class CanvasControllerImpl implements CanvasController {
return this.model.drawData; return this.model.drawData;
} }
public get interactionData(): InteractionData {
return this.model.interactionData;
}
public get mergeData(): MergeData { public get mergeData(): MergeData {
return this.model.mergeData; return this.model.mergeData;
} }
@ -140,6 +163,10 @@ export class CanvasControllerImpl implements CanvasController {
return this.model.selected; return this.model.selected;
} }
public get configuration(): Configuration {
return this.model.configuration;
}
public set mode(value: Mode) { public set mode(value: Mode) {
this.model.mode = value; this.model.mode = value;
} }

@ -43,7 +43,7 @@ export interface ActiveElement {
export enum RectDrawingMethod { export enum RectDrawingMethod {
CLASSIC = 'By 2 points', CLASSIC = 'By 2 points',
EXTREME_POINTS = 'By 4 points' EXTREME_POINTS = 'By 4 points',
} }
export enum CuboidDrawingMethod { export enum CuboidDrawingMethod {
@ -56,6 +56,7 @@ export interface Configuration {
displayAllText?: boolean; displayAllText?: boolean;
undefinedAttrValue?: string; undefinedAttrValue?: string;
showProjections?: boolean; showProjections?: boolean;
forceDisableEditing?: boolean;
} }
export interface DrawData { export interface DrawData {
@ -69,6 +70,20 @@ export interface DrawData {
redraw?: number; redraw?: number;
} }
export interface InteractionData {
enabled: boolean;
shapeType?: string;
crosshair?: boolean;
minPosVertices?: number;
minNegVertices?: number;
}
export interface InteractionResult {
points: number[];
shapeType: string;
button: number;
}
export interface EditData { export interface EditData {
enabled: boolean; enabled: boolean;
state: any; state: any;
@ -99,12 +114,14 @@ export enum UpdateReasons {
IMAGE_MOVED = 'image_moved', IMAGE_MOVED = 'image_moved',
GRID_UPDATED = 'grid_updated', GRID_UPDATED = 'grid_updated',
ISSUE_REGIONS_UPDATED = 'issue_regions_updated',
OBJECTS_UPDATED = 'objects_updated', OBJECTS_UPDATED = 'objects_updated',
SHAPE_ACTIVATED = 'shape_activated', SHAPE_ACTIVATED = 'shape_activated',
SHAPE_FOCUSED = 'shape_focused', SHAPE_FOCUSED = 'shape_focused',
FITTED_CANVAS = 'fitted_canvas', FITTED_CANVAS = 'fitted_canvas',
INTERACT = 'interact',
DRAW = 'draw', DRAW = 'draw',
MERGE = 'merge', MERGE = 'merge',
SPLIT = 'split', SPLIT = 'split',
@ -112,9 +129,11 @@ export enum UpdateReasons {
SELECT = 'select', SELECT = 'select',
CANCEL = 'cancel', CANCEL = 'cancel',
BITMAP = 'bitmap', BITMAP = 'bitmap',
SELECT_REGION = 'select_region',
DRAG_CANVAS = 'drag_canvas', DRAG_CANVAS = 'drag_canvas',
ZOOM_CANVAS = 'zoom_canvas', ZOOM_CANVAS = 'zoom_canvas',
CONFIG_UPDATED = 'config_updated', CONFIG_UPDATED = 'config_updated',
DATA_FAILED = 'data_failed',
} }
export enum Mode { export enum Mode {
@ -126,6 +145,8 @@ export enum Mode {
MERGE = 'merge', MERGE = 'merge',
SPLIT = 'split', SPLIT = 'split',
GROUP = 'group', GROUP = 'group',
INTERACT = 'interact',
SELECT_REGION = 'select_region',
DRAG_CANVAS = 'drag_canvas', DRAG_CANVAS = 'drag_canvas',
ZOOM_CANVAS = 'zoom_canvas', ZOOM_CANVAS = 'zoom_canvas',
} }
@ -133,12 +154,14 @@ export enum Mode {
export interface CanvasModel { export interface CanvasModel {
readonly imageBitmap: boolean; readonly imageBitmap: boolean;
readonly image: Image | null; readonly image: Image | null;
readonly issueRegions: Record<number, number[]>;
readonly objects: any[]; readonly objects: any[];
readonly zLayer: number | null; readonly zLayer: number | null;
readonly gridSize: Size; readonly gridSize: Size;
readonly focusData: FocusData; readonly focusData: FocusData;
readonly activeElement: ActiveElement; readonly activeElement: ActiveElement;
readonly drawData: DrawData; readonly drawData: DrawData;
readonly interactionData: InteractionData;
readonly mergeData: MergeData; readonly mergeData: MergeData;
readonly splitData: SplitData; readonly splitData: SplitData;
readonly groupData: GroupData; readonly groupData: GroupData;
@ -146,11 +169,13 @@ export interface CanvasModel {
readonly selected: any; readonly selected: any;
geometry: Geometry; geometry: Geometry;
mode: Mode; mode: Mode;
exception: Error | null;
zoom(x: number, y: number, direction: number): void; zoom(x: number, y: number, direction: number): void;
move(topOffset: number, leftOffset: number): void; move(topOffset: number, leftOffset: number): void;
setup(frameData: any, objectStates: any[], zLayer: number): void; setup(frameData: any, objectStates: any[], zLayer: number): void;
setupIssueRegions(issueRegions: Record<number, number[]>): void;
activate(clientID: number | null, attributeID: number | null): void; activate(clientID: number | null, attributeID: number | null): void;
rotate(rotationAngle: number): void; rotate(rotationAngle: number): void;
focus(clientID: number, padding: number): void; focus(clientID: number, padding: number): void;
@ -162,9 +187,11 @@ export interface CanvasModel {
split(splitData: SplitData): void; split(splitData: SplitData): void;
merge(mergeData: MergeData): void; merge(mergeData: MergeData): void;
select(objectState: any): void; select(objectState: any): void;
interact(interactionData: InteractionData): void;
fitCanvas(width: number, height: number): void; fitCanvas(width: number, height: number): void;
bitmap(enabled: boolean): void; bitmap(enabled: boolean): void;
selectRegion(enabled: boolean): void;
dragCanvas(enable: boolean): void; dragCanvas(enable: boolean): void;
zoomCanvas(enable: boolean): void; zoomCanvas(enable: boolean): void;
@ -188,15 +215,18 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
gridSize: Size; gridSize: Size;
left: number; left: number;
objects: any[]; objects: any[];
issueRegions: Record<number, number[]>;
scale: number; scale: number;
top: number; top: number;
zLayer: number | null; zLayer: number | null;
drawData: DrawData; drawData: DrawData;
interactionData: InteractionData;
mergeData: MergeData; mergeData: MergeData;
groupData: GroupData; groupData: GroupData;
splitData: SplitData; splitData: SplitData;
selected: any; selected: any;
mode: Mode; mode: Mode;
exception: Error | null;
}; };
public constructor() { public constructor() {
@ -235,6 +265,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}, },
left: 0, left: 0,
objects: [], objects: [],
issueRegions: {},
scale: 1, scale: 1,
top: 0, top: 0,
zLayer: null, zLayer: null,
@ -242,6 +273,9 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
enabled: false, enabled: false,
initialState: null, initialState: null,
}, },
interactionData: {
enabled: false,
},
mergeData: { mergeData: {
enabled: false, enabled: false,
}, },
@ -253,28 +287,29 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}, },
selected: null, selected: null,
mode: Mode.IDLE, mode: Mode.IDLE,
exception: null,
}; };
} }
public zoom(x: number, y: number, direction: number): void { public zoom(x: number, y: number, direction: number): void {
const oldScale: number = this.data.scale; const oldScale: number = this.data.scale;
const newScale: number = direction > 0 ? oldScale * 6 / 5 : oldScale * 5 / 6; const newScale: number = direction > 0 ? (oldScale * 6) / 5 : (oldScale * 5) / 6;
this.data.scale = Math.min(Math.max(newScale, FrameZoom.MIN), FrameZoom.MAX); this.data.scale = Math.min(Math.max(newScale, FrameZoom.MIN), FrameZoom.MAX);
const { angle } = this.data; const { angle } = this.data;
const mutiplier = Math.sin(angle * Math.PI / 180) + Math.cos(angle * Math.PI / 180); const mutiplier = Math.sin((angle * Math.PI) / 180) + Math.cos((angle * Math.PI) / 180);
if ((angle / 90) % 2) { if ((angle / 90) % 2) {
// 90, 270, .. // 90, 270, ..
this.data.top += mutiplier * ((x - this.data.imageSize.width / 2) const topMultiplier = (x - this.data.imageSize.width / 2) * (oldScale / this.data.scale - 1);
* (oldScale / this.data.scale - 1)) * this.data.scale; const leftMultiplier = (y - this.data.imageSize.height / 2) * (oldScale / this.data.scale - 1);
this.data.left -= mutiplier * ((y - this.data.imageSize.height / 2) this.data.top += mutiplier * topMultiplier * this.data.scale;
* (oldScale / this.data.scale - 1)) * this.data.scale; this.data.left -= mutiplier * leftMultiplier * this.data.scale;
} else { } else {
this.data.left += mutiplier * ((x - this.data.imageSize.width / 2) const leftMultiplier = (x - this.data.imageSize.width / 2) * (oldScale / this.data.scale - 1);
* (oldScale / this.data.scale - 1)) * this.data.scale; const topMultiplier = (y - this.data.imageSize.height / 2) * (oldScale / this.data.scale - 1);
this.data.top += mutiplier * ((y - this.data.imageSize.height / 2) this.data.left += mutiplier * leftMultiplier * this.data.scale;
* (oldScale / this.data.scale - 1)) * this.data.scale; this.data.top += mutiplier * topMultiplier * this.data.scale;
} }
this.notify(UpdateReasons.IMAGE_ZOOMED); this.notify(UpdateReasons.IMAGE_ZOOMED);
@ -290,10 +325,9 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.data.canvasSize.height = height; this.data.canvasSize.height = height;
this.data.canvasSize.width = width; this.data.canvasSize.width = width;
this.data.imageOffset = Math.floor(Math.max( this.data.imageOffset = Math.floor(
this.data.canvasSize.height / FrameZoom.MIN, Math.max(this.data.canvasSize.height / FrameZoom.MIN, this.data.canvasSize.width / FrameZoom.MIN),
this.data.canvasSize.width / FrameZoom.MIN, );
));
this.notify(UpdateReasons.FITTED_CANVAS); this.notify(UpdateReasons.FITTED_CANVAS);
this.notify(UpdateReasons.OBJECTS_UPDATED); this.notify(UpdateReasons.OBJECTS_UPDATED);
@ -304,6 +338,19 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.notify(UpdateReasons.BITMAP); this.notify(UpdateReasons.BITMAP);
} }
public selectRegion(enable: boolean): void {
if (enable && this.data.mode !== Mode.IDLE) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
if (!enable && this.data.mode !== Mode.SELECT_REGION) {
throw Error(`Canvas is not in the region selecting mode. Action: ${this.data.mode}`);
}
this.data.mode = enable ? Mode.SELECT_REGION : Mode.IDLE;
this.notify(UpdateReasons.SELECT_REGION);
}
public dragCanvas(enable: boolean): void { public dragCanvas(enable: boolean): void {
if (enable && this.data.mode !== Mode.IDLE) { if (enable && this.data.mode !== Mode.IDLE) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`); throw Error(`Canvas is busy. Action: ${this.data.mode}`);
@ -345,36 +392,42 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
} }
this.data.imageID = frameData.number; this.data.imageID = frameData.number;
frameData.data( frameData
(): void => { .data((): void => {
this.data.image = null; this.data.image = null;
this.notify(UpdateReasons.IMAGE_CHANGED); this.notify(UpdateReasons.IMAGE_CHANGED);
}, })
).then((data: Image): void => { .then((data: Image): void => {
if (frameData.number !== this.data.imageID) { if (frameData.number !== this.data.imageID) {
// already another image // already another image
return; return;
} }
this.data.imageSize = { this.data.imageSize = {
height: (frameData.height as number), height: frameData.height as number,
width: (frameData.width as number), width: frameData.width as number,
}; };
this.data.image = data; this.data.image = data;
this.notify(UpdateReasons.IMAGE_CHANGED); this.notify(UpdateReasons.IMAGE_CHANGED);
this.data.zLayer = zLayer; this.data.zLayer = zLayer;
this.data.objects = objectStates; this.data.objects = objectStates;
this.notify(UpdateReasons.OBJECTS_UPDATED); this.notify(UpdateReasons.OBJECTS_UPDATED);
}).catch((exception: any): void => { })
throw exception; .catch((exception: any): void => {
}); this.data.exception = exception;
this.notify(UpdateReasons.DATA_FAILED);
throw exception;
});
}
public setupIssueRegions(issueRegions: Record<number, number[]>): void {
this.data.issueRegions = issueRegions;
this.notify(UpdateReasons.ISSUE_REGIONS_UPDATED);
} }
public activate(clientID: number | null, attributeID: number | null): void { public activate(clientID: number | null, attributeID: number | null): void {
if (this.data.activeElement.clientID === clientID if (this.data.activeElement.clientID === clientID && this.data.activeElement.attributeID === attributeID) {
&& this.data.activeElement.attributeID === attributeID
) {
return; return;
} }
@ -382,9 +435,8 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
throw Error(`Canvas is busy. Action: ${this.data.mode}`); throw Error(`Canvas is busy. Action: ${this.data.mode}`);
} }
if (typeof (clientID) === 'number') { if (typeof clientID === 'number') {
const [state] = this.objects const [state] = this.objects.filter((_state: any): boolean => _state.clientID === clientID);
.filter((_state: any): boolean => _state.clientID === clientID);
if (!state || state.objectType === 'tag') { if (!state || state.objectType === 'tag') {
return; return;
} }
@ -400,7 +452,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
public rotate(rotationAngle: number): void { public rotate(rotationAngle: number): void {
if (this.data.angle !== rotationAngle) { if (this.data.angle !== rotationAngle) {
this.data.angle = (360 + Math.floor((rotationAngle) / 90) * 90) % 360; this.data.angle = (360 + Math.floor(rotationAngle / 90) * 90) % 360;
this.fit(); this.fit();
} }
} }
@ -430,13 +482,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
); );
} }
this.data.scale = Math.min( this.data.scale = Math.min(Math.max(this.data.scale, FrameZoom.MIN), FrameZoom.MAX);
Math.max(this.data.scale, FrameZoom.MIN),
FrameZoom.MAX,
);
this.data.top = (this.data.canvasSize.height / 2 - this.data.imageSize.height / 2); 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.data.left = this.data.canvasSize.width / 2 - this.data.imageSize.width / 2;
this.notify(UpdateReasons.IMAGE_FITTED); this.notify(UpdateReasons.IMAGE_FITTED);
} }
@ -460,7 +509,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
throw new Error('Drawing has been already started'); throw new Error('Drawing has been already started');
} else if (!drawData.shapeType && !drawData.initialState) { } else if (!drawData.shapeType && !drawData.initialState) {
throw new Error('A shape type is not specified'); throw new Error('A shape type is not specified');
} else if (typeof (drawData.numberOfPoints) !== 'undefined') { } else if (typeof drawData.numberOfPoints !== 'undefined') {
if (drawData.shapeType === 'polygon' && drawData.numberOfPoints < 3) { if (drawData.shapeType === 'polygon' && drawData.numberOfPoints < 3) {
throw new Error('A polygon consists of at least 3 points'); throw new Error('A polygon consists of at least 3 points');
} else if (drawData.shapeType === 'polyline' && drawData.numberOfPoints < 2) { } else if (drawData.shapeType === 'polyline' && drawData.numberOfPoints < 2) {
@ -469,10 +518,9 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
} }
} }
if (typeof (drawData.redraw) === 'number') { if (typeof drawData.redraw === 'number') {
const clientID = drawData.redraw; const clientID = drawData.redraw;
const [state] = this.data.objects const [state] = this.data.objects.filter((_state: any): boolean => _state.clientID === clientID);
.filter((_state: any): boolean => _state.clientID === clientID);
if (state) { if (state) {
this.data.drawData = { ...drawData }; this.data.drawData = { ...drawData };
@ -490,6 +538,27 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.notify(UpdateReasons.DRAW); this.notify(UpdateReasons.DRAW);
} }
public interact(interactionData: InteractionData): void {
if (![Mode.IDLE, Mode.INTERACT].includes(this.data.mode)) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
if (interactionData.enabled) {
if (this.data.interactionData.enabled) {
throw new Error('Interaction has been already started');
} else if (!interactionData.shapeType) {
throw new Error('A shape type was not specified');
}
}
this.data.interactionData = interactionData;
if (typeof this.data.interactionData.crosshair !== 'boolean') {
this.data.interactionData.crosshair = true;
}
this.notify(UpdateReasons.INTERACT);
}
public split(splitData: SplitData): void { public split(splitData: SplitData): void {
if (![Mode.IDLE, Mode.SPLIT].includes(this.data.mode)) { if (![Mode.IDLE, Mode.SPLIT].includes(this.data.mode)) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`); throw Error(`Canvas is busy. Action: ${this.data.mode}`);
@ -548,27 +617,31 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
} }
public configure(configuration: Configuration): void { public configure(configuration: Configuration): void {
if (typeof (configuration.displayAllText) !== 'undefined') { if (typeof configuration.displayAllText !== 'undefined') {
this.data.configuration.displayAllText = configuration.displayAllText; this.data.configuration.displayAllText = configuration.displayAllText;
} }
if (typeof (configuration.showProjections) !== 'undefined') { if (typeof configuration.showProjections !== 'undefined') {
this.data.configuration.showProjections = configuration.showProjections; this.data.configuration.showProjections = configuration.showProjections;
} }
if (typeof (configuration.autoborders) !== 'undefined') { if (typeof configuration.autoborders !== 'undefined') {
this.data.configuration.autoborders = configuration.autoborders; this.data.configuration.autoborders = configuration.autoborders;
} }
if (typeof (configuration.undefinedAttrValue) !== 'undefined') { if (typeof configuration.undefinedAttrValue !== 'undefined') {
this.data.configuration.undefinedAttrValue = configuration.undefinedAttrValue; this.data.configuration.undefinedAttrValue = configuration.undefinedAttrValue;
} }
if (typeof configuration.forceDisableEditing !== 'undefined') {
this.data.configuration.forceDisableEditing = configuration.forceDisableEditing;
}
this.notify(UpdateReasons.CONFIG_UPDATED); this.notify(UpdateReasons.CONFIG_UPDATED);
} }
public isAbleToChangeFrame(): boolean { public isAbleToChangeFrame(): boolean {
const isUnable = [Mode.DRAG, Mode.EDIT, Mode.RESIZE].includes(this.data.mode) const isUnable = [Mode.DRAG, Mode.EDIT, Mode.RESIZE, Mode.INTERACT].includes(this.data.mode)
|| (this.data.mode === Mode.DRAW && typeof (this.data.drawData.redraw) === 'number'); || (this.data.mode === Mode.DRAW && typeof this.data.drawData.redraw === 'number');
return !isUnable; return !isUnable;
} }
@ -604,10 +677,9 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.data.imageOffset = geometry.offset; this.data.imageOffset = geometry.offset;
this.data.scale = geometry.scale; this.data.scale = geometry.scale;
this.data.imageOffset = Math.floor(Math.max( this.data.imageOffset = Math.floor(
this.data.canvasSize.height / FrameZoom.MIN, Math.max(this.data.canvasSize.height / FrameZoom.MIN, this.data.canvasSize.width / FrameZoom.MIN),
this.data.canvasSize.width / FrameZoom.MIN, );
));
} }
public get zLayer(): number | null { public get zLayer(): number | null {
@ -622,10 +694,13 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
return this.data.image; return this.data.image;
} }
public get issueRegions(): Record<number, number[]> {
return { ...this.data.issueRegions };
}
public get objects(): any[] { public get objects(): any[] {
if (this.data.zLayer !== null) { if (this.data.zLayer !== null) {
return this.data.objects return this.data.objects.filter((object: any): boolean => object.zOrder <= this.data.zLayer);
.filter((object: any): boolean => object.zOrder <= this.data.zLayer);
} }
return this.data.objects; return this.data.objects;
@ -647,6 +722,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
return { ...this.data.drawData }; return { ...this.data.drawData };
} }
public get interactionData(): InteractionData {
return { ...this.data.interactionData };
}
public get mergeData(): MergeData { public get mergeData(): MergeData {
return { ...this.data.mergeData }; return { ...this.data.mergeData };
} }
@ -670,4 +749,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
public get mode(): Mode { public get mode(): Mode {
return this.data.mode; return this.data.mode;
} }
public get exception(): Error {
return this.data.exception;
}
} }

File diff suppressed because it is too large Load Diff

@ -2,13 +2,13 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
const BASE_STROKE_WIDTH = 1.75; const BASE_STROKE_WIDTH = 1.25;
const BASE_GRID_WIDTH = 2; const BASE_GRID_WIDTH = 2;
const BASE_POINT_SIZE = 5; const BASE_POINT_SIZE = 5;
const TEXT_MARGIN = 10; const TEXT_MARGIN = 10;
const AREA_THRESHOLD = 9; const AREA_THRESHOLD = 9;
const SIZE_THRESHOLD = 3; const SIZE_THRESHOLD = 3;
const POINTS_STROKE_WIDTH = 1.5; const POINTS_STROKE_WIDTH = 1;
const POINTS_SELECTED_STROKE_WIDTH = 4; const POINTS_SELECTED_STROKE_WIDTH = 4;
const MIN_EDGE_LENGTH = 3; const MIN_EDGE_LENGTH = 3;
const CUBOID_ACTIVE_EDGE_STROKE_WIDTH = 2.5; const CUBOID_ACTIVE_EDGE_STROKE_WIDTH = 2.5;
@ -16,6 +16,7 @@ const CUBOID_UNACTIVE_EDGE_STROKE_WIDTH = 1.75;
const UNDEFINED_ATTRIBUTE_VALUE = '__undefined__'; const UNDEFINED_ATTRIBUTE_VALUE = '__undefined__';
const ARROW_PATH = 'M13.162 6.284L.682.524a.483.483 0 0 0-.574.134.477.477 0 ' const ARROW_PATH = 'M13.162 6.284L.682.524a.483.483 0 0 0-.574.134.477.477 0 '
+ '0 0-.012.59L4.2 6.72.096 12.192a.479.479 0 0 0 .585.724l12.48-5.76a.48.48 0 0 0 0-.872z'; + '0 0-.012.59L4.2 6.72.096 12.192a.479.479 0 0 0 .585.724l12.48-5.76a.48.48 0 0 0 0-.872z';
const BASE_PATTERN_SIZE = 5;
export default { export default {
BASE_STROKE_WIDTH, BASE_STROKE_WIDTH,
@ -31,4 +32,5 @@ export default {
CUBOID_UNACTIVE_EDGE_STROKE_WIDTH, CUBOID_UNACTIVE_EDGE_STROKE_WIDTH,
UNDEFINED_ATTRIBUTE_VALUE, UNDEFINED_ATTRIBUTE_VALUE,
ARROW_PATH, ARROW_PATH,
BASE_PATTERN_SIZE,
}; };

@ -0,0 +1,76 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import * as SVG from 'svg.js';
import consts from './consts';
export default class Crosshair {
private x: SVG.Line | null;
private y: SVG.Line | null;
private canvas: SVG.Container | null;
public constructor() {
this.x = null;
this.y = null;
this.canvas = null;
}
public show(canvas: SVG.Container, x: number, y: number, scale: number): void {
if (this.canvas && this.canvas !== canvas) {
if (this.x) this.x.remove();
if (this.y) this.y.remove();
this.x = null;
this.y = null;
}
this.canvas = canvas;
this.x = this.canvas
.line(0, y, this.canvas.node.clientWidth, y)
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * scale),
})
.addClass('cvat_canvas_crosshair');
this.y = this.canvas
.line(x, 0, x, this.canvas.node.clientHeight)
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * scale),
})
.addClass('cvat_canvas_crosshair');
}
public hide(): void {
if (this.x) {
this.x.remove();
this.x = null;
}
if (this.y) {
this.y.remove();
this.y = null;
}
this.canvas = null;
}
public move(x: number, y: number): void {
if (this.x) {
this.x.attr({ y1: y, y2: y });
}
if (this.y) {
this.y.attr({ x1: x, x2: x });
}
}
public scale(scale: number): void {
if (this.x) {
this.x.attr('stroke-width', consts.BASE_STROKE_WIDTH / (2 * scale));
}
if (this.y) {
this.y.attr('stroke-width', consts.BASE_STROKE_WIDTH / (2 * scale));
}
}
}

@ -1,12 +1,3 @@
/* eslint-disable func-names */
/* eslint-disable no-underscore-dangle */
/* eslint-disable curly */
/*
* Copyright (C) 2020 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
import consts from './consts'; import consts from './consts';
export interface Point { export interface Point {
@ -26,9 +17,7 @@ function line(p1: Point, p2: Point): number[] {
return [a, b, c]; return [a, b, c];
} }
function intersection( function intersection(p1: Point, p2: Point, p3: Point, p4: Point): Point | null {
p1: Point, p2: Point, p3: Point, p4: Point,
): Point | null {
const L1 = line(p1, p2); const L1 = line(p1, p2);
const L2 = line(p3, p4); const L2 = line(p3, p4);
@ -246,8 +235,20 @@ export class CuboidModel {
this.rb = new Edge([3, 5], this.points); this.rb = new Edge([3, 5], this.points);
this.db = new Edge([7, 5], this.points); this.db = new Edge([7, 5], this.points);
this.edgeList = [this.fl, this.fr, this.dl, this.dr, this.ft, this.lt, this.edgeList = [
this.rt, this.dt, this.fb, this.lb, this.rb, this.db]; this.fl,
this.fr,
this.dl,
this.dr,
this.ft,
this.lt,
this.rt,
this.dt,
this.fb,
this.lb,
this.rb,
this.db,
];
} }
private initFaces(): void { private initFaces(): void {
@ -324,7 +325,7 @@ function sortPointsClockwise(points: any[]): any[] {
let ang = Math.atan2(point.y - center.y, point.x - center.x); let ang = Math.atan2(point.y - center.y, point.x - center.x);
if (!startAng) { if (!startAng) {
startAng = ang; startAng = ang;
// ensure that all points are clockwise of the start point // ensure that all points are clockwise of the start point
} else if (ang < startAng) { } else if (ang < startAng) {
ang += Math.PI * 2; ang += Math.PI * 2;
} }
@ -347,18 +348,18 @@ function setupCuboidPoints(points: Point[]): any[] {
let p3; let p3;
let p4; let p4;
const height = Math.abs(points[0].x - points[1].x) const height =
< Math.abs(points[1].x - points[2].x) Math.abs(points[0].x - points[1].x) < Math.abs(points[1].x - points[2].x)
? Math.abs(points[1].y - points[0].y) ? Math.abs(points[1].y - points[0].y)
: Math.abs(points[1].y - points[2].y); : Math.abs(points[1].y - points[2].y);
// seperate into left and right point // seperate into left and right point
// we pick the first and third point because we know assume they will be on // we pick the first and third point because we know assume they will be on
// opposite corners // opposite corners
if (points[0].x < points[2].x) { if (points[0].x < points[2].x) {
[left,, right] = points; [left, , right] = points;
} else { } else {
[right,, left] = points; [right, , left] = points;
} }
// get other 2 points using the given height // get other 2 points using the given height
@ -408,7 +409,7 @@ export function cuboidFrom4Points(flattenedPoints: any[]): any[] {
points.push({ x, y }); points.push({ x, y });
} }
const unsortedPlanePoints = points.slice(0, 3); const unsortedPlanePoints = points.slice(0, 3);
function rotate(array: any[], times: number): void{ function rotate(array: any[], times: number): void {
let t = times; let t = times;
while (t--) { while (t--) {
const temp = array.shift(); const temp = array.shift();
@ -460,28 +461,21 @@ export function cuboidFrom4Points(flattenedPoints: any[]): any[] {
plane2.p3 = { x: plane1.p3.x + vec.x, y: plane1.p3.y + vec.y }; plane2.p3 = { x: plane1.p3.x + vec.x, y: plane1.p3.y + vec.y };
plane2.p4 = { x: plane1.p4.x + vec.x, y: plane1.p4.y + vec.y }; plane2.p4 = { x: plane1.p4.x + vec.x, y: plane1.p4.y + vec.y };
let cuboidPoints; let cuboidPoints;
// right // right
if (Math.abs(angle) < Math.PI / 2 - 0.1) { if (Math.abs(angle) < Math.PI / 2 - 0.1) {
cuboidPoints = setupCuboidPoints(points); cuboidPoints = setupCuboidPoints(points);
// left // left
} else if (Math.abs(angle) > Math.PI / 2 + 0.1) { } else if (Math.abs(angle) > Math.PI / 2 + 0.1) {
cuboidPoints = setupCuboidPoints(points); cuboidPoints = setupCuboidPoints(points);
// down // down
} else if (angle > 0) { } else if (angle > 0) {
cuboidPoints = [ cuboidPoints = [plane1.p1, plane2.p1, plane1.p2, plane2.p2, plane1.p3, plane2.p3, plane1.p4, plane2.p4];
plane1.p1, plane2.p1, plane1.p2, plane2.p2,
plane1.p3, plane2.p3, plane1.p4, plane2.p4,
];
cuboidPoints[0].y += 0.1; cuboidPoints[0].y += 0.1;
cuboidPoints[4].y += 0.1; cuboidPoints[4].y += 0.1;
// up // up
} else { } else {
cuboidPoints = [ cuboidPoints = [plane2.p1, plane1.p1, plane2.p2, plane1.p2, plane2.p3, plane1.p3, plane2.p4, plane1.p4];
plane2.p1, plane1.p1, plane2.p2, plane1.p2,
plane2.p3, plane1.p3, plane2.p4, plane1.p4,
];
cuboidPoints[0].y += 0.1; cuboidPoints[0].y += 0.1;
cuboidPoints[4].y += 0.1; cuboidPoints[4].y += 0.1;
} }

@ -16,14 +16,9 @@ import {
BBox, BBox,
Box, Box,
} from './shared'; } from './shared';
import Crosshair from './crosshair';
import consts from './consts'; import consts from './consts';
import { import { DrawData, Geometry, RectDrawingMethod, Configuration, CuboidDrawingMethod } from './canvasModel';
DrawData,
Geometry,
RectDrawingMethod,
Configuration,
CuboidDrawingMethod,
} from './canvasModel';
import { cuboidFrom4Points } from './cuboid'; import { cuboidFrom4Points } from './cuboid';
@ -44,10 +39,7 @@ export class DrawHandlerImpl implements DrawHandler {
x: number; x: number;
y: number; y: number;
}; };
private crosshair: { private crosshair: Crosshair;
x: SVG.Line;
y: SVG.Line;
};
private drawData: DrawData; private drawData: DrawData;
private geometry: Geometry; private geometry: Geometry;
private autoborderHandler: AutoborderHandler; private autoborderHandler: AutoborderHandler;
@ -66,8 +58,9 @@ export class DrawHandlerImpl implements DrawHandler {
const frameHeight = this.geometry.image.height; const frameHeight = this.geometry.image.height;
const { offset } = this.geometry; const { offset } = this.geometry;
let [xtl, ytl, xbr, ybr] = [bbox.x, bbox.y, bbox.x + bbox.width, bbox.y + bbox.height] let [xtl, ytl, xbr, ybr] = [bbox.x, bbox.y, bbox.x + bbox.width, bbox.y + bbox.height].map(
.map((coord: number): number => coord - offset); (coord: number): number => coord - offset,
);
xtl = Math.min(Math.max(xtl, 0), frameWidth); xtl = Math.min(Math.max(xtl, 0), frameWidth);
xbr = Math.min(Math.max(xbr, 0), frameWidth); xbr = Math.min(Math.max(xbr, 0), frameWidth);
@ -77,7 +70,9 @@ export class DrawHandlerImpl implements DrawHandler {
return [xtl, ytl, xbr, ybr]; return [xtl, ytl, xbr, ybr];
} }
private getFinalPolyshapeCoordinates(targetPoints: number[]): { private getFinalPolyshapeCoordinates(
targetPoints: number[],
): {
points: number[]; points: number[];
box: Box; box: Box;
} { } {
@ -108,7 +103,9 @@ export class DrawHandlerImpl implements DrawHandler {
}; };
} }
private getFinalCuboidCoordinates(targetPoints: number[]): { private getFinalCuboidCoordinates(
targetPoints: number[],
): {
points: number[]; points: number[];
box: Box; box: Box;
} { } {
@ -135,8 +132,7 @@ export class DrawHandlerImpl implements DrawHandler {
for (let i = 0; i < points.length - 1; i += 2) { for (let i = 0; i < points.length - 1; i += 2) {
const [x, y] = points.slice(i); const [x, y] = points.slice(i);
if (x >= offset && x <= offset + frameWidth if (x >= offset && x <= offset + frameWidth && y >= offset && y <= offset + frameHeight) continue;
&& y >= offset && y <= offset + frameHeight) continue;
let xOffset = 0; let xOffset = 0;
let yOffset = 0; let yOffset = 0;
@ -158,9 +154,8 @@ export class DrawHandlerImpl implements DrawHandler {
if (cuboidOffsets.length === points.length / 2) { if (cuboidOffsets.length === points.length / 2) {
cuboidOffsets.forEach((offsetCoords: number[]): void => { cuboidOffsets.forEach((offsetCoords: number[]): void => {
if (Math.sqrt((offsetCoords[0] ** 2) + (offsetCoords[1] ** 2)) if (Math.sqrt(offsetCoords[0] ** 2 + offsetCoords[1] ** 2) < minCuboidOffset.d) {
< minCuboidOffset.d) { minCuboidOffset.d = Math.sqrt(offsetCoords[0] ** 2 + offsetCoords[1] ** 2);
minCuboidOffset.d = Math.sqrt((offsetCoords[0] ** 2) + (offsetCoords[1] ** 2));
[minCuboidOffset.dx, minCuboidOffset.dy] = offsetCoords; [minCuboidOffset.dx, minCuboidOffset.dy] = offsetCoords;
} }
}); });
@ -188,22 +183,11 @@ export class DrawHandlerImpl implements DrawHandler {
private addCrosshair(): void { private addCrosshair(): void {
const { x, y } = this.cursorPosition; const { x, y } = this.cursorPosition;
this.crosshair = { this.crosshair.show(this.canvas, x, y, this.geometry.scale);
x: this.canvas.line(0, y, this.canvas.node.clientWidth, y).attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * this.geometry.scale),
zOrder: Number.MAX_SAFE_INTEGER,
}).addClass('cvat_canvas_crosshair'),
y: this.canvas.line(x, 0, x, 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 { private removeCrosshair(): void {
this.crosshair.x.remove(); this.crosshair.hide();
this.crosshair.y.remove();
this.crosshair = null;
} }
private release(): void { private release(): void {
@ -228,8 +212,10 @@ export class DrawHandlerImpl implements DrawHandler {
// Or when no drawn points, but we call cancel() drawing // Or when no drawn points, but we call cancel() drawing
// We check if it is activated with remember function // We check if it is activated with remember function
if (this.drawInstance.remember('_paintHandler')) { if (this.drawInstance.remember('_paintHandler')) {
if (this.drawData.shapeType !== 'rectangle' if (
&& this.drawData.cuboidDrawingMethod !== CuboidDrawingMethod.CLASSIC) { this.drawData.shapeType !== 'rectangle' &&
this.drawData.cuboidDrawingMethod !== CuboidDrawingMethod.CLASSIC
) {
// Check for unsaved drawn shapes // Check for unsaved drawn shapes
this.drawInstance.draw('done'); this.drawInstance.draw('done');
} }
@ -261,37 +247,48 @@ export class DrawHandlerImpl implements DrawHandler {
private drawBox(): void { private drawBox(): void {
this.drawInstance = this.canvas.rect(); this.drawInstance = this.canvas.rect();
this.drawInstance.on('drawstop', (e: Event): void => { this.drawInstance
const bbox = (e.target as SVGRectElement).getBBox(); .on('drawstop', (e: Event): void => {
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox); const bbox = (e.target as SVGRectElement).getBBox();
const { shapeType, redraw: clientID } = this.drawData; const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
this.release(); const { shapeType, redraw: clientID } = this.drawData;
this.release();
if (this.canceled) return; if (this.canceled) return;
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) { if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
this.onDrawDone({ this.onDrawDone(
clientID, {
shapeType, clientID,
points: [xtl, ytl, xbr, ybr], shapeType,
}, Date.now() - this.startTimestamp); points: [xtl, ytl, xbr, ybr],
} },
}).on('drawupdate', (): void => { Date.now() - this.startTimestamp,
this.shapeSizeElement.update(this.drawInstance); );
}).addClass('cvat_canvas_shape_drawing').attr({ }
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, })
}); .on('drawupdate', (): void => {
this.shapeSizeElement.update(this.drawInstance);
})
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
} }
private drawBoxBy4Points(): void { private drawBoxBy4Points(): void {
let numberOfPoints = 0; let numberOfPoints = 0;
this.drawInstance = (this.canvas as any).polygon() this.drawInstance = (this.canvas as any)
.addClass('cvat_canvas_shape_drawing').attr({ .polygon()
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': 0, 'stroke-width': 0,
opacity: 0, opacity: 0,
}).on('drawstart', (): void => { })
.on('drawstart', (): void => {
// init numberOfPoints as one on drawstart // init numberOfPoints as one on drawstart
numberOfPoints = 1; numberOfPoints = 1;
}).on('drawpoint', (e: CustomEvent): void => { })
.on('drawpoint', (e: CustomEvent): void => {
// increase numberOfPoints by one on drawpoint // increase numberOfPoints by one on drawpoint
numberOfPoints += 1; numberOfPoints += 1;
@ -303,14 +300,18 @@ export class DrawHandlerImpl implements DrawHandler {
this.cancel(); this.cancel();
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) { if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
this.onDrawDone({ this.onDrawDone(
shapeType, {
clientID, shapeType,
points: [xtl, ytl, xbr, ybr], clientID,
}, Date.now() - this.startTimestamp); points: [xtl, ytl, xbr, ybr],
},
Date.now() - this.startTimestamp,
);
} }
} }
}).on('undopoint', (): void => { })
.on('undopoint', (): void => {
if (numberOfPoints > 0) { if (numberOfPoints > 0) {
numberOfPoints -= 1; numberOfPoints -= 1;
} }
@ -324,12 +325,16 @@ export class DrawHandlerImpl implements DrawHandler {
const sizeDecrement = (): void => { const sizeDecrement = (): void => {
if (--size === 0) { if (--size === 0) {
this.drawInstance.draw('done'); // we need additional settimeout because we cannot invoke draw('done')
// from event listener for drawstart event
// because of implementation of svg.js
setTimeout((): void => this.drawInstance.draw('done'));
} }
}; };
this.drawInstance.on('drawstart', sizeDecrement); this.drawInstance.on('drawstart', sizeDecrement);
this.drawInstance.on('drawpoint', sizeDecrement); this.drawInstance.on('drawpoint', sizeDecrement);
this.drawInstance.on('drawupdate', (): void => this.transform(this.geometry));
this.drawInstance.on('undopoint', (): number => size++); this.drawInstance.on('undopoint', (): number => size++);
// Add ability to cancel the latest drawn point // Add ability to cancel the latest drawn point
@ -360,10 +365,7 @@ export class DrawHandlerImpl implements DrawHandler {
} else { } else {
this.drawInstance.draw('update', e); this.drawInstance.draw('update', e);
const deltaTreshold = 15; const deltaTreshold = 15;
const delta = Math.sqrt( const delta = Math.sqrt((e.clientX - lastDrawnPoint.x) ** 2 + (e.clientY - lastDrawnPoint.y) ** 2);
((e.clientX - lastDrawnPoint.x) ** 2)
+ ((e.clientY - lastDrawnPoint.y) ** 2),
);
if (delta > deltaTreshold) { if (delta > deltaTreshold) {
this.drawInstance.draw('point', e); this.drawInstance.draw('point', e);
} }
@ -384,50 +386,67 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawInstance.on('drawdone', (e: CustomEvent): void => { this.drawInstance.on('drawdone', (e: CustomEvent): void => {
const targetPoints = pointsToNumberArray((e.target as SVGElement).getAttribute('points')); const targetPoints = pointsToNumberArray((e.target as SVGElement).getAttribute('points'));
const { shapeType, redraw: clientID } = this.drawData; const { shapeType, redraw: clientID } = this.drawData;
const { points, box } = shapeType === 'cuboid' ? this.getFinalCuboidCoordinates(targetPoints) const { points, box } =
: this.getFinalPolyshapeCoordinates(targetPoints); shapeType === 'cuboid'
? this.getFinalCuboidCoordinates(targetPoints)
: this.getFinalPolyshapeCoordinates(targetPoints);
this.release(); this.release();
if (this.canceled) return; if (this.canceled) return;
if (shapeType === 'polygon' if (
&& ((box.xbr - box.xtl) * (box.ybr - box.ytl) >= consts.AREA_THRESHOLD) shapeType === 'polygon' &&
&& points.length >= 3 * 2) { (box.xbr - box.xtl) * (box.ybr - box.ytl) >= consts.AREA_THRESHOLD &&
this.onDrawDone({ points.length >= 3 * 2
clientID, ) {
shapeType, this.onDrawDone(
points, {
}, Date.now() - this.startTimestamp); clientID,
} else if (shapeType === 'polyline' shapeType,
&& ((box.xbr - box.xtl) >= consts.SIZE_THRESHOLD points,
|| (box.ybr - box.ytl) >= consts.SIZE_THRESHOLD) },
&& points.length >= 2 * 2) { Date.now() - this.startTimestamp,
this.onDrawDone({ );
clientID, } else if (
shapeType, shapeType === 'polyline' &&
points, (box.xbr - box.xtl >= consts.SIZE_THRESHOLD || box.ybr - box.ytl >= consts.SIZE_THRESHOLD) &&
}, Date.now() - this.startTimestamp); points.length >= 2 * 2
} else if (shapeType === 'points' ) {
&& (e.target as any).getAttribute('points') !== '0,0') { this.onDrawDone(
this.onDrawDone({ {
clientID, clientID,
shapeType, shapeType,
points, points,
}, Date.now() - this.startTimestamp); },
Date.now() - this.startTimestamp,
);
} else if (shapeType === 'points' && (e.target as any).getAttribute('points') !== '0,0') {
this.onDrawDone(
{
clientID,
shapeType,
points,
},
Date.now() - this.startTimestamp,
);
// TODO: think about correct constraign for cuboids // TODO: think about correct constraign for cuboids
} else if (shapeType === 'cuboid' } else if (shapeType === 'cuboid' && points.length === 4 * 2) {
&& points.length === 4 * 2) { this.onDrawDone(
this.onDrawDone({ {
clientID, clientID,
shapeType, shapeType,
points: cuboidFrom4Points(points), points: cuboidFrom4Points(points),
}, Date.now() - this.startTimestamp); },
Date.now() - this.startTimestamp,
);
} }
}); });
} }
private drawPolygon(): void { private drawPolygon(): void {
this.drawInstance = (this.canvas as any).polygon() this.drawInstance = (this.canvas as any)
.addClass('cvat_canvas_shape_drawing').attr({ .polygon()
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
}); });
@ -438,8 +457,10 @@ export class DrawHandlerImpl implements DrawHandler {
} }
private drawPolyline(): void { private drawPolyline(): void {
this.drawInstance = (this.canvas as any).polyline() this.drawInstance = (this.canvas as any)
.addClass('cvat_canvas_shape_drawing').attr({ .polyline()
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': 0, 'fill-opacity': 0,
}); });
@ -451,18 +472,19 @@ export class DrawHandlerImpl implements DrawHandler {
} }
private drawPoints(): void { private drawPoints(): void {
this.drawInstance = (this.canvas as any).polygon() this.drawInstance = (this.canvas as any).polygon().addClass('cvat_canvas_shape_drawing').attr({
.addClass('cvat_canvas_shape_drawing').attr({ 'stroke-width': 0,
'stroke-width': 0, opacity: 0,
opacity: 0, });
});
this.drawPolyshape(); this.drawPolyshape();
} }
private drawCuboidBy4Points(): void { private drawCuboidBy4Points(): void {
this.drawInstance = (this.canvas as any).polyline() this.drawInstance = (this.canvas as any)
.addClass('cvat_canvas_shape_drawing').attr({ .polyline()
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
}); });
this.drawPolyshape(); this.drawPolyshape();
@ -470,25 +492,32 @@ export class DrawHandlerImpl implements DrawHandler {
private drawCuboid(): void { private drawCuboid(): void {
this.drawInstance = this.canvas.rect(); this.drawInstance = this.canvas.rect();
this.drawInstance.on('drawstop', (e: Event): void => { this.drawInstance
const bbox = (e.target as SVGRectElement).getBBox(); .on('drawstop', (e: Event): void => {
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox); const bbox = (e.target as SVGRectElement).getBBox();
const { shapeType } = this.drawData; const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
this.release(); const { shapeType } = this.drawData;
this.release();
if (this.canceled) return; if (this.canceled) return;
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) { if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
const d = { x: (xbr - xtl) * 0.1, y: (ybr - ytl) * 0.1 }; const d = { x: (xbr - xtl) * 0.1, y: (ybr - ytl) * 0.1 };
this.onDrawDone({ this.onDrawDone(
shapeType, {
points: cuboidFrom4Points([xtl, ybr, xbr, ybr, xbr, ytl, xbr + d.x, ytl - d.y]), shapeType,
}, Date.now() - this.startTimestamp); points: cuboidFrom4Points([xtl, ybr, xbr, ybr, xbr, ytl, xbr + d.x, ytl - d.y]),
} },
}).on('drawupdate', (): void => { Date.now() - this.startTimestamp,
this.shapeSizeElement.update(this.drawInstance); );
}).addClass('cvat_canvas_shape_drawing').attr({ }
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, })
}); .on('drawupdate', (): void => {
this.shapeSizeElement.update(this.drawInstance);
})
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
} }
private pastePolyshape(): void { private pastePolyshape(): void {
@ -498,22 +527,28 @@ export class DrawHandlerImpl implements DrawHandler {
.split(/[,\s]/g) .split(/[,\s]/g)
.map((coord: string): number => +coord); .map((coord: string): number => +coord);
const { points } = this.drawData.initialState.shapeType === 'cuboid' ? this.getFinalCuboidCoordinates(targetPoints) const { points } =
: this.getFinalPolyshapeCoordinates(targetPoints); this.drawData.initialState.shapeType === 'cuboid'
? this.getFinalCuboidCoordinates(targetPoints)
: this.getFinalPolyshapeCoordinates(targetPoints);
if (!e.detail.originalEvent.ctrlKey) { if (!e.detail.originalEvent.ctrlKey) {
this.release(); this.release();
} }
this.onDrawDone({ this.onDrawDone(
shapeType: this.drawData.initialState.shapeType, {
objectType: this.drawData.initialState.objectType, shapeType: this.drawData.initialState.shapeType,
points, objectType: this.drawData.initialState.objectType,
occluded: this.drawData.initialState.occluded, points,
attributes: { ...this.drawData.initialState.attributes }, occluded: this.drawData.initialState.occluded,
label: this.drawData.initialState.label, attributes: { ...this.drawData.initialState.attributes },
color: this.drawData.initialState.color, label: this.drawData.initialState.label,
}, Date.now() - this.startTimestamp, e.detail.originalEvent.ctrlKey); color: this.drawData.initialState.color,
},
Date.now() - this.startTimestamp,
e.detail.originalEvent.ctrlKey,
);
}); });
} }
@ -534,9 +569,11 @@ export class DrawHandlerImpl implements DrawHandler {
} }
private pasteBox(box: BBox): void { private pasteBox(box: BBox): void {
this.drawInstance = (this.canvas as any).rect(box.width, box.height) this.drawInstance = (this.canvas as any)
.rect(box.width, box.height)
.move(box.x, box.y) .move(box.x, box.y)
.addClass('cvat_canvas_shape_drawing').attr({ .addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
}); });
this.pasteShape(); this.pasteShape();
@ -548,22 +585,27 @@ export class DrawHandlerImpl implements DrawHandler {
this.release(); this.release();
} }
this.onDrawDone({ this.onDrawDone(
shapeType: this.drawData.initialState.shapeType, {
objectType: this.drawData.initialState.objectType, shapeType: this.drawData.initialState.shapeType,
points: [xtl, ytl, xbr, ybr], objectType: this.drawData.initialState.objectType,
occluded: this.drawData.initialState.occluded, points: [xtl, ytl, xbr, ybr],
attributes: { ...this.drawData.initialState.attributes }, occluded: this.drawData.initialState.occluded,
label: this.drawData.initialState.label, attributes: { ...this.drawData.initialState.attributes },
color: this.drawData.initialState.color, label: this.drawData.initialState.label,
}, Date.now() - this.startTimestamp, e.detail.originalEvent.ctrlKey); color: this.drawData.initialState.color,
},
Date.now() - this.startTimestamp,
e.detail.originalEvent.ctrlKey,
);
}); });
} }
private pastePolygon(points: string): void { private pastePolygon(points: string): void {
this.drawInstance = (this.canvas as any).polygon(points) this.drawInstance = (this.canvas as any)
.addClass('cvat_canvas_shape_drawing').attr({ .polygon(points)
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
}); });
this.pasteShape(); this.pasteShape();
@ -571,8 +613,10 @@ export class DrawHandlerImpl implements DrawHandler {
} }
private pastePolyline(points: string): void { private pastePolyline(points: string): void {
this.drawInstance = (this.canvas as any).polyline(points) this.drawInstance = (this.canvas as any)
.addClass('cvat_canvas_shape_drawing').attr({ .polyline(points)
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
}); });
this.pasteShape(); this.pasteShape();
@ -580,22 +624,19 @@ export class DrawHandlerImpl implements DrawHandler {
} }
private pasteCuboid(points: string): void { private pasteCuboid(points: string): void {
this.drawInstance = (this.canvas as any).cube(points).addClass('cvat_canvas_shape_drawing').attr({ this.drawInstance = (this.canvas as any)
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, .cube(points)
'face-stroke': 'black', .addClass('cvat_canvas_shape_drawing')
}); .attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'face-stroke': 'black',
});
this.pasteShape(); this.pasteShape();
this.pastePolyshape(); this.pastePolyshape();
} }
private pastePoints(initialPoints: string): void { private pastePoints(initialPoints: string): void {
function moveShape( function moveShape(shape: SVG.PolyLine, group: SVG.G, x: number, y: number, scale: number): void {
shape: SVG.PolyLine,
group: SVG.G,
x: number,
y: number,
scale: number,
): void {
const bbox = shape.bbox(); const bbox = shape.bbox();
shape.move(x - bbox.width / 2, y - bbox.height / 2); shape.move(x - bbox.width / 2, y - bbox.height / 2);
@ -610,10 +651,9 @@ export class DrawHandlerImpl implements DrawHandler {
const { x: initialX, y: initialY } = this.cursorPosition; const { x: initialX, y: initialY } = this.cursorPosition;
this.pointsGroup = this.canvas.group(); this.pointsGroup = this.canvas.group();
this.drawInstance = (this.canvas as any).polyline(initialPoints) this.drawInstance = (this.canvas as any).polyline(initialPoints).addClass('cvat_canvas_shape_drawing').style({
.addClass('cvat_canvas_shape_drawing').style({ 'stroke-width': 0,
'stroke-width': 0, });
});
let numOfPoints = initialPoints.split(' ').length; let numOfPoints = initialPoints.split(' ').length;
while (numOfPoints) { while (numOfPoints) {
@ -626,15 +666,11 @@ export class DrawHandlerImpl implements DrawHandler {
}); });
} }
moveShape( moveShape(this.drawInstance, this.pointsGroup, initialX, initialY, this.geometry.scale);
this.drawInstance, this.pointsGroup, initialX, initialY, this.geometry.scale,
);
this.canvas.on('mousemove.draw', (): void => { this.canvas.on('mousemove.draw', (): void => {
const { x, y } = this.cursorPosition; // was computer in another callback const { x, y } = this.cursorPosition; // was computer in another callback
moveShape( moveShape(this.drawInstance, this.pointsGroup, x, y, this.geometry.scale);
this.drawInstance, this.pointsGroup, x, y, this.geometry.scale,
);
}); });
this.pastePolyshape(); this.pastePolyshape();
@ -668,8 +704,9 @@ export class DrawHandlerImpl implements DrawHandler {
if (this.drawData.initialState) { if (this.drawData.initialState) {
const { offset } = this.geometry; const { offset } = this.geometry;
if (this.drawData.shapeType === 'rectangle') { if (this.drawData.shapeType === 'rectangle') {
const [xtl, ytl, xbr, ybr] = this.drawData.initialState.points const [xtl, ytl, xbr, ybr] = this.drawData.initialState.points.map(
.map((coord: number): number => coord + offset); (coord: number): number => coord + offset,
);
this.pasteBox({ this.pasteBox({
x: xtl, x: xtl,
@ -678,8 +715,7 @@ export class DrawHandlerImpl implements DrawHandler {
height: ybr - ytl, height: ybr - ytl,
}); });
} else { } else {
const points = this.drawData.initialState.points const points = this.drawData.initialState.points.map((coord: number): number => coord + offset);
.map((coord: number): number => coord + offset);
const stringifiedPoints = stringifyPoints(points); const stringifiedPoints = stringifyPoints(points);
if (this.drawData.shapeType === 'polygon') { if (this.drawData.shapeType === 'polygon') {
@ -741,7 +777,7 @@ export class DrawHandlerImpl implements DrawHandler {
this.canceled = false; this.canceled = false;
this.drawData = null; this.drawData = null;
this.geometry = null; this.geometry = null;
this.crosshair = null; this.crosshair = new Crosshair();
this.drawInstance = null; this.drawInstance = null;
this.pointsGroup = null; this.pointsGroup = null;
this.cursorPosition = { this.cursorPosition = {
@ -750,28 +786,20 @@ export class DrawHandlerImpl implements DrawHandler {
}; };
this.canvas.on('mousemove.crosshair', (e: MouseEvent): void => { this.canvas.on('mousemove.crosshair', (e: MouseEvent): void => {
const [x, y] = translateToSVG( const [x, y] = translateToSVG((this.canvas.node as any) as SVGSVGElement, [e.clientX, e.clientY]);
this.canvas.node as any as SVGSVGElement,
[e.clientX, e.clientY],
);
this.cursorPosition = { x, y }; this.cursorPosition = { x, y };
if (this.crosshair) { if (this.crosshair) {
this.crosshair.x.attr({ y1: y, y2: y }); this.crosshair.move(x, y);
this.crosshair.y.attr({ x1: x, x2: x });
} }
}); });
} }
public configurate(configuration: Configuration): void { public configurate(configuration: Configuration): void {
if (typeof (configuration.autoborders) === 'boolean') { if (typeof configuration.autoborders === 'boolean') {
this.autobordersEnabled = configuration.autoborders; this.autobordersEnabled = configuration.autoborders;
if (this.drawInstance) { if (this.drawInstance) {
if (this.autobordersEnabled) { if (this.autobordersEnabled) {
this.autoborderHandler.autoborder( this.autoborderHandler.autoborder(true, this.drawInstance, this.drawData.redraw);
true,
this.drawInstance,
this.drawData.redraw,
);
} else { } else {
this.autoborderHandler.autoborder(false); this.autoborderHandler.autoborder(false);
} }
@ -787,12 +815,7 @@ export class DrawHandlerImpl implements DrawHandler {
} }
if (this.crosshair) { if (this.crosshair) {
this.crosshair.x.attr({ this.crosshair.scale(this.geometry.scale);
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * geometry.scale),
});
this.crosshair.y.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * geometry.scale),
});
} }
if (this.pointsGroup) { if (this.pointsGroup) {
@ -813,14 +836,8 @@ export class DrawHandlerImpl implements DrawHandler {
const paintHandler = this.drawInstance.remember('_paintHandler'); const paintHandler = this.drawInstance.remember('_paintHandler');
for (const point of (paintHandler as any).set.members) { for (const point of (paintHandler as any).set.members) {
point.attr( point.attr('stroke-width', `${consts.POINTS_STROKE_WIDTH / geometry.scale}`);
'stroke-width', point.attr('r', `${consts.BASE_POINT_SIZE / geometry.scale}`);
`${consts.POINTS_STROKE_WIDTH / geometry.scale}`,
);
point.attr(
'r',
`${consts.BASE_POINT_SIZE / geometry.scale}`,
);
} }
} }
} }

@ -47,7 +47,8 @@ export class EditHandlerImpl implements EditHandler {
if (e.button !== 0) return; if (e.button !== 0) return;
const { offset } = this.geometry; const { offset } = this.geometry;
const stringifiedPoints = `${head} ${this.editLine.node.getAttribute('points').slice(0, -2)}`; const stringifiedPoints = `${head} ${this.editLine.node.getAttribute('points').slice(0, -2)}`;
const points = pointsToNumberArray(stringifiedPoints).slice(0, -2) const points = pointsToNumberArray(stringifiedPoints)
.slice(0, -2)
.map((coord: number): number => coord - offset); .map((coord: number): number => coord - offset);
if (points.length >= minimumPoints * 2) { if (points.length >= minimumPoints * 2) {
@ -63,7 +64,7 @@ export class EditHandlerImpl implements EditHandler {
private startEdit(): void { private startEdit(): void {
// get started coordinates // get started coordinates
const [clientX, clientY] = translateFromSVG( const [clientX, clientY] = translateFromSVG(
this.canvas.node as any as SVGSVGElement, (this.canvas.node as any) as SVGSVGElement,
this.editedShape.attr('points').split(' ')[this.editData.pointID].split(','), this.editedShape.attr('points').split(' ')[this.editData.pointID].split(','),
); );
@ -92,10 +93,7 @@ export class EditHandlerImpl implements EditHandler {
(this.editLine as any).draw('point', e); (this.editLine as any).draw('point', e);
} else { } else {
const deltaTreshold = 15; const deltaTreshold = 15;
const delta = Math.sqrt( const delta = Math.sqrt((e.clientX - lastDrawnPoint.x) ** 2 + (e.clientY - lastDrawnPoint.y) ** 2);
((e.clientX - lastDrawnPoint.x) ** 2)
+ ((e.clientY - lastDrawnPoint.y) ** 2),
);
if (delta > deltaTreshold) { if (delta > deltaTreshold) {
(this.editLine as any).draw('point', e); (this.editLine as any).draw('point', e);
} }
@ -112,17 +110,23 @@ export class EditHandlerImpl implements EditHandler {
} }
const strokeColor = this.editedShape.attr('stroke'); const strokeColor = this.editedShape.attr('stroke');
(this.editLine as any).addClass('cvat_canvas_shape_drawing').style({ (this.editLine as any)
'pointer-events': 'none', .addClass('cvat_canvas_shape_drawing')
'fill-opacity': 0, .style({
'stroke': strokeColor, 'pointer-events': 'none',
}).attr({ 'fill-opacity': 0,
'data-origin-client-id': this.editData.state.clientID, stroke: strokeColor,
}).on('drawstart drawpoint', (e: CustomEvent): void => { })
this.transform(this.geometry); .attr({
lastDrawnPoint.x = e.detail.event.clientX; 'data-origin-client-id': this.editData.state.clientID,
lastDrawnPoint.y = e.detail.event.clientY; })
}).draw(dummyEvent, { snapToGrid: 0.1 }); .on('drawstart drawpoint', (e: CustomEvent): void => {
this.transform(this.geometry);
lastDrawnPoint.x = e.detail.event.clientX;
lastDrawnPoint.y = e.detail.event.clientY;
})
.on('drawupdate', (): void => this.transform(this.geometry))
.draw(dummyEvent, { snapToGrid: 0.1 });
if (this.editData.state.shapeType === 'points') { if (this.editData.state.shapeType === 'points') {
this.editLine.attr('stroke-width', 0); this.editLine.attr('stroke-width', 0);
@ -140,9 +144,7 @@ export class EditHandlerImpl implements EditHandler {
if (e.button === 0 && !e.altKey) { if (e.button === 0 && !e.altKey) {
(this.editLine as any).draw('point', e); (this.editLine as any).draw('point', e);
} else if (e.button === 2 && this.editLine) { } else if (e.button === 2 && this.editLine) {
if (this.editData.state.shapeType === 'points' if (this.editData.state.shapeType === 'points' || this.editLine.attr('points').split(' ').length > 2) {
|| this.editLine.attr('points').split(' ').length > 2
) {
(this.editLine as any).draw('undo'); (this.editLine as any).draw('undo');
} }
} }
@ -151,8 +153,7 @@ export class EditHandlerImpl implements EditHandler {
private selectPolygon(shape: SVG.Polygon): void { private selectPolygon(shape: SVG.Polygon): void {
const { offset } = this.geometry; const { offset } = this.geometry;
const points = pointsToNumberArray(shape.attr('points')) const points = pointsToNumberArray(shape.attr('points')).map((coord: number): number => coord - offset);
.map((coord: number): number => coord - offset);
const { state } = this.editData; const { state } = this.editData;
this.edit({ this.edit({
@ -167,8 +168,7 @@ export class EditHandlerImpl implements EditHandler {
} }
// Get stop point and all points // Get stop point and all points
const stopPointID = Array.prototype.indexOf const stopPointID = Array.prototype.indexOf.call((e.target as HTMLElement).parentElement.children, e.target);
.call((e.target as HTMLElement).parentElement.children, e.target);
const oldPoints = this.editedShape.attr('points').trim().split(' '); const oldPoints = this.editedShape.attr('points').trim().split(' ');
const linePoints = this.editLine.attr('points').trim().split(' '); const linePoints = this.editLine.attr('points').trim().split(' ');
@ -178,8 +178,7 @@ export class EditHandlerImpl implements EditHandler {
} }
// Compute new point array // Compute new point array
const [start, stop] = [this.editData.pointID, stopPointID] const [start, stop] = [this.editData.pointID, stopPointID].sort((a, b): number => +a - +b);
.sort((a, b): number => +a - +b);
if (this.editData.state.shapeType !== 'polygon') { if (this.editData.state.shapeType !== 'polygon') {
let points = null; let points = null;
@ -189,15 +188,15 @@ export class EditHandlerImpl implements EditHandler {
if (start !== this.editData.pointID) { if (start !== this.editData.pointID) {
linePoints.reverse(); linePoints.reverse();
} }
points = oldPoints.slice(0, start) points = oldPoints
.slice(0, start)
.concat(linePoints) .concat(linePoints)
.concat(oldPoints.slice(stop + 1)); .concat(oldPoints.slice(stop + 1));
} else { } else {
points = oldPoints.concat(linePoints.slice(0, -1)); points = oldPoints.concat(linePoints.slice(0, -1));
} }
points = pointsToNumberArray(points.join(' ')) points = pointsToNumberArray(points.join(' ')).map((coord: number): number => coord - offset);
.map((coord: number): number => coord - offset);
const { state } = this.editData; const { state } = this.editData;
this.edit({ this.edit({
@ -208,25 +207,27 @@ export class EditHandlerImpl implements EditHandler {
return; return;
} }
const cutIndexes1 = oldPoints.reduce((acc: string[], _: string, i: number) => const cutIndexes1 = oldPoints.reduce(
i >= stop || i <= start ? [...acc, i] : acc, []); (acc: string[], _: string, i: number) => (i >= stop || i <= start ? [...acc, i] : acc),
const cutIndexes2 = oldPoints.reduce((acc: string[], _: string, i: number) => [],
i <= stop && i >= start ? [...acc, i] : acc, []); );
const cutIndexes2 = oldPoints.reduce(
(acc: string[], _: string, i: number) => (i <= stop && i >= start ? [...acc, i] : acc),
[],
);
const curveLength = (indexes: number[]) => { const curveLength = (indexes: number[]): number => {
const points = indexes.map((index: number): string => oldPoints[index]) const points = indexes
.map((index: number): string => oldPoints[index])
.map((point: string): string[] => point.split(',')) .map((point: string): string[] => point.split(','))
.map((point: string[]): number[] => [+point[0], +point[1]]); .map((point: string[]): number[] => [+point[0], +point[1]]);
let length = 0; let length = 0;
for (let i = 1; i < points.length; i++) { for (let i = 1; i < points.length; i++) {
length += Math.sqrt( length += Math.sqrt((points[i][0] - points[i - 1][0]) ** 2 + (points[i][1] - points[i - 1][1]) ** 2);
(points[i][0] - points[i - 1][0]) ** 2
+ (points[i][1] - points[i - 1][1]) ** 2,
);
} }
return length; return length;
} };
const pointsCriteria = cutIndexes1.length > cutIndexes2.length; const pointsCriteria = cutIndexes1.length > cutIndexes2.length;
const lengthCriteria = curveLength(cutIndexes1) > curveLength(cutIndexes2); const lengthCriteria = curveLength(cutIndexes1) > curveLength(cutIndexes2);
@ -235,11 +236,11 @@ export class EditHandlerImpl implements EditHandler {
linePoints.reverse(); linePoints.reverse();
} }
const firstPart = oldPoints.slice(0, start) const firstPart = oldPoints
.slice(0, start)
.concat(linePoints) .concat(linePoints)
.concat(oldPoints.slice(stop + 1)); .concat(oldPoints.slice(stop + 1));
const secondPart = oldPoints.slice(start, stop) const secondPart = oldPoints.slice(start, stop).concat(linePoints.slice(1).reverse());
.concat(linePoints.slice(1).reverse());
if (firstPart.length < 3 || secondPart.length < 3) { if (firstPart.length < 3 || secondPart.length < 3) {
this.cancel(); this.cancel();
@ -263,23 +264,26 @@ export class EditHandlerImpl implements EditHandler {
this.selectPolygon(this.clones[0]); this.selectPolygon(this.clones[0]);
} else { } else {
for (const points of [firstPart, secondPart]) { for (const points of [firstPart, secondPart]) {
this.clones.push(this.canvas.polygon(points.join(' ')) this.clones.push(
.attr('fill', this.editedShape.attr('fill')) this.canvas
.attr('fill-opacity', '0.5') .polygon(points.join(' '))
.addClass('cvat_canvas_shape')); .attr('fill', this.editedShape.attr('fill'))
.attr('fill-opacity', '0.5')
.addClass('cvat_canvas_shape'),
);
} }
for (const clone of this.clones) { for (const clone of this.clones) {
clone.on('click', (): void => this.selectPolygon(clone)); clone.on('click', (): void => this.selectPolygon(clone));
clone.on('mouseenter', (): void => { clone
clone.addClass('cvat_canvas_shape_splitting'); .on('mouseenter', (): void => {
}).on('mouseleave', (): void => { clone.addClass('cvat_canvas_shape_splitting');
clone.removeClass('cvat_canvas_shape_splitting'); })
}); .on('mouseleave', (): void => {
clone.removeClass('cvat_canvas_shape_splitting');
});
} }
} }
return;
} }
private setupPoints(enabled: boolean): void { private setupPoints(enabled: boolean): void {
@ -289,7 +293,7 @@ export class EditHandlerImpl implements EditHandler {
if (enabled) { if (enabled) {
(this.editedShape as any).selectize(true, { (this.editedShape as any).selectize(true, {
deepSelect: true, deepSelect: true,
pointSize: 2 * consts.BASE_POINT_SIZE / self.geometry.scale, pointSize: (2 * consts.BASE_POINT_SIZE) / self.geometry.scale,
rotationPoint: false, rotationPoint: false,
pointType(cx: number, cy: number): SVG.Circle { pointType(cx: number, cy: number): SVG.Circle {
const circle: SVG.Circle = this.nested const circle: SVG.Circle = this.nested
@ -355,9 +359,7 @@ export class EditHandlerImpl implements EditHandler {
} }
private initEditing(): void { private initEditing(): void {
this.editedShape = this.canvas this.editedShape = this.canvas.select(`#cvat_canvas_shape_${this.editData.state.clientID}`).first().clone();
.select(`#cvat_canvas_shape_${this.editData.state.clientID}`)
.first().clone();
this.setupPoints(true); this.setupPoints(true);
this.startEdit(); this.startEdit();
// draw points for this with selected and start editing till another point is clicked // draw points for this with selected and start editing till another point is clicked
@ -407,15 +409,11 @@ export class EditHandlerImpl implements EditHandler {
} }
public configurate(configuration: Configuration): void { public configurate(configuration: Configuration): void {
if (typeof (configuration.autoborders) === 'boolean') { if (typeof configuration.autoborders === 'boolean') {
this.autobordersEnabled = configuration.autoborders; this.autobordersEnabled = configuration.autoborders;
if (this.editLine) { if (this.editLine) {
if (this.autobordersEnabled) { if (this.autobordersEnabled) {
this.autoborderHandler.autoborder( this.autoborderHandler.autoborder(true, this.editLine, this.editData.state.clientID);
true,
this.editLine,
this.editData.state.clientID,
);
} else { } else {
this.autoborderHandler.autoborder(false); this.autoborderHandler.autoborder(false);
} }
@ -443,14 +441,8 @@ export class EditHandlerImpl implements EditHandler {
const paintHandler = this.editLine.remember('_paintHandler'); const paintHandler = this.editLine.remember('_paintHandler');
for (const point of (paintHandler as any).set.members) { for (const point of (paintHandler as any).set.members) {
point.attr( point.attr('stroke-width', `${consts.POINTS_STROKE_WIDTH / geometry.scale}`);
'stroke-width', point.attr('r', `${consts.BASE_POINT_SIZE / geometry.scale}`);
`${consts.POINTS_STROKE_WIDTH / geometry.scale}`,
);
point.attr(
'r',
`${consts.BASE_POINT_SIZE / geometry.scale}`,
);
} }
} }
} }

@ -5,10 +5,7 @@
import * as SVG from 'svg.js'; import * as SVG from 'svg.js';
import { GroupData } from './canvasModel'; import { GroupData } from './canvasModel';
import { import { translateToSVG } from './shared';
translateToSVG,
} from './shared';
export interface GroupHandler { export interface GroupHandler {
group(groupData: GroupData): void; group(groupData: GroupData): void;
@ -35,16 +32,15 @@ export class GroupHandlerImpl implements GroupHandler {
private statesToBeGroupped: any[]; private statesToBeGroupped: any[];
private highlightedShapes: Record<number, SVG.Shape>; private highlightedShapes: Record<number, SVG.Shape>;
private getSelectionBox(event: MouseEvent): { private getSelectionBox(
xtl: number; event: MouseEvent,
ytl: number; ): {
xbr: number; xtl: number;
ybr: number; ytl: number;
} { xbr: number;
const point = translateToSVG( ybr: number;
(this.canvas.node as any as SVGSVGElement), } {
[event.clientX, event.clientY], const point = translateToSVG((this.canvas.node as any) as SVGSVGElement, [event.clientX, event.clientY]);
);
const stopSelectionPoint = { const stopSelectionPoint = {
x: point[0], x: point[0],
y: point[1], y: point[1],
@ -60,10 +56,7 @@ export class GroupHandlerImpl implements GroupHandler {
private onSelectStart(event: MouseEvent): void { private onSelectStart(event: MouseEvent): void {
if (!this.selectionRect) { if (!this.selectionRect) {
const point = translateToSVG( const point = translateToSVG((this.canvas.node as any) as SVGSVGElement, [event.clientX, event.clientY]);
this.canvas.node as any as SVGSVGElement,
[event.clientX, event.clientY],
);
this.startSelectionPoint = { this.startSelectionPoint = {
x: point[0], x: point[0],
y: point[1], y: point[1],
@ -102,12 +95,16 @@ export class GroupHandlerImpl implements GroupHandler {
// TODO: Doesn't work properly for groups // TODO: Doesn't work properly for groups
const bbox = shape.bbox(); const bbox = shape.bbox();
const clientID = shape.attr('clientID'); const clientID = shape.attr('clientID');
if (bbox.x > box.xtl && bbox.y > box.ytl if (
bbox.x > box.xtl
&& bbox.y > box.ytl
&& bbox.x + bbox.width < box.xbr && bbox.x + bbox.width < box.xbr
&& bbox.y + bbox.height < box.ybr && bbox.y + bbox.height < box.ybr
&& !(clientID in this.highlightedShapes)) { && !(clientID in this.highlightedShapes)
const objectState = this.getStates() ) {
.filter((state: any): boolean => state.clientID === clientID)[0]; const objectState = this.getStates().filter(
(state: any): boolean => state.clientID === clientID,
)[0];
if (objectState) { if (objectState) {
this.statesToBeGroupped.push(objectState); this.statesToBeGroupped.push(objectState);
@ -127,7 +124,6 @@ export class GroupHandlerImpl implements GroupHandler {
this.resetSelectedObjects(); this.resetSelectedObjects();
this.initialized = false; this.initialized = false;
this.selectionRect = null;
this.startSelectionPoint = { this.startSelectionPoint = {
x: null, x: null,
y: null, y: null,
@ -216,6 +212,10 @@ export class GroupHandlerImpl implements GroupHandler {
} }
this.statesToBeGroupped = []; this.statesToBeGroupped = [];
this.highlightedShapes = {}; this.highlightedShapes = {};
if (this.selectionRect) {
this.selectionRect.remove();
this.selectionRect = null;
}
} }
public cancel(): void { public cancel(): void {

@ -0,0 +1,268 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import * as SVG from 'svg.js';
import consts from './consts';
import Crosshair from './crosshair';
import { translateToSVG } from './shared';
import { InteractionData, InteractionResult, Geometry } from './canvasModel';
export interface InteractionHandler {
transform(geometry: Geometry): void;
interact(interactData: InteractionData): void;
cancel(): void;
}
export class InteractionHandlerImpl implements InteractionHandler {
private onInteraction: (shapes: InteractionResult[] | null, shapesUpdated?: boolean, isDone?: boolean) => void;
private geometry: Geometry;
private canvas: SVG.Container;
private interactionData: InteractionData;
private cursorPosition: { x: number; y: number };
private shapesWereUpdated: boolean;
private interactionShapes: SVG.Shape[];
private currentInteractionShape: SVG.Shape | null;
private crosshair: Crosshair;
private prepareResult(): InteractionResult[] {
return this.interactionShapes.map(
(shape: SVG.Shape): InteractionResult => {
if (shape.type === 'circle') {
const points = [(shape as SVG.Circle).cx(), (shape as SVG.Circle).cy()];
return {
points: points.map((coord: number): number => coord - this.geometry.offset),
shapeType: 'points',
button: shape.attr('stroke') === 'green' ? 0 : 2,
};
}
const bbox = ((shape.node as any) as SVGRectElement).getBBox();
const points = [bbox.x, bbox.y, bbox.x + bbox.width, bbox.y + bbox.height];
return {
points: points.map((coord: number): number => coord - this.geometry.offset),
shapeType: 'rectangle',
button: 0,
};
},
);
}
private shouldRaiseEvent(ctrlKey: boolean): boolean {
const { interactionData, interactionShapes, shapesWereUpdated } = this;
const { minPosVertices, minNegVertices, enabled } = interactionData;
const positiveShapes = interactionShapes.filter(
(shape: SVG.Shape): boolean => (shape as any).attr('stroke') === 'green',
);
const negativeShapes = interactionShapes.filter(
(shape: SVG.Shape): boolean => (shape as any).attr('stroke') !== 'green',
);
if (interactionData.shapeType === 'rectangle') {
return enabled && !ctrlKey && !!interactionShapes.length;
}
const minimumVerticesAchieved =
(typeof minPosVertices === 'undefined' || minPosVertices <= positiveShapes.length) &&
(typeof minNegVertices === 'undefined' || minPosVertices <= negativeShapes.length);
return enabled && !ctrlKey && minimumVerticesAchieved && shapesWereUpdated;
}
private addCrosshair(): void {
const { x, y } = this.cursorPosition;
this.crosshair.show(this.canvas, x, y, this.geometry.scale);
}
private removeCrosshair(): void {
this.crosshair.hide();
}
private interactPoints(): void {
const eventListener = (e: MouseEvent): void => {
if ((e.button === 0 || e.button === 2) && !e.altKey) {
e.preventDefault();
const [cx, cy] = translateToSVG((this.canvas.node as any) as SVGSVGElement, [e.clientX, e.clientY]);
this.currentInteractionShape = this.canvas
.circle((consts.BASE_POINT_SIZE * 2) / this.geometry.scale)
.center(cx, cy)
.fill('white')
.stroke(e.button === 0 ? 'green' : 'red')
.addClass('cvat_interaction_point')
.attr({
'stroke-width': consts.POINTS_STROKE_WIDTH / this.geometry.scale,
});
this.interactionShapes.push(this.currentInteractionShape);
this.shapesWereUpdated = true;
if (this.shouldRaiseEvent(e.ctrlKey)) {
this.onInteraction(this.prepareResult(), true, false);
}
const self = this.currentInteractionShape;
self.on('mouseenter', (): void => {
self.attr({
'stroke-width': consts.POINTS_SELECTED_STROKE_WIDTH / this.geometry.scale,
});
self.on('mousedown', (_e: MouseEvent): void => {
_e.preventDefault();
_e.stopPropagation();
self.remove();
this.interactionShapes = this.interactionShapes.filter(
(shape: SVG.Shape): boolean => shape !== self,
);
this.shapesWereUpdated = true;
if (this.shouldRaiseEvent(_e.ctrlKey)) {
this.onInteraction(this.prepareResult(), true, false);
}
});
});
self.on('mouseleave', (): void => {
self.attr({
'stroke-width': consts.POINTS_STROKE_WIDTH / this.geometry.scale,
});
self.off('mousedown');
});
}
};
// clear this listener in relese()
this.canvas.on('mousedown.interaction', eventListener);
}
private interactRectangle(): void {
let initialized = false;
const eventListener = (e: MouseEvent): void => {
if (e.button === 0 && !e.altKey) {
if (!initialized) {
(this.currentInteractionShape as any).draw(e, { snapToGrid: 0.1 });
initialized = true;
} else {
(this.currentInteractionShape as any).draw(e);
}
}
};
this.currentInteractionShape = this.canvas.rect();
this.canvas.on('mousedown.interaction', eventListener);
this.currentInteractionShape
.on('drawstop', (): void => {
this.interactionShapes.push(this.currentInteractionShape);
this.shapesWereUpdated = true;
this.canvas.off('mousedown.interaction', eventListener);
this.interact({ enabled: false });
})
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
}
private initInteraction(): void {
if (this.interactionData.crosshair) {
this.addCrosshair();
}
}
private startInteraction(): void {
if (this.interactionData.shapeType === 'rectangle') {
this.interactRectangle();
} else if (this.interactionData.shapeType === 'points') {
this.interactPoints();
} else {
throw new Error('Interactor implementation supports only rectangle and points');
}
}
private release(): void {
if (this.crosshair) {
this.removeCrosshair();
}
this.canvas.off('mousedown.interaction');
this.interactionShapes.forEach((shape: SVG.Shape): SVG.Shape => shape.remove());
this.interactionShapes = [];
if (this.currentInteractionShape) {
this.currentInteractionShape.remove();
this.currentInteractionShape = null;
}
}
public constructor(
onInteraction: (shapes: InteractionResult[] | null, shapesUpdated?: boolean, isDone?: boolean) => void,
canvas: SVG.Container,
geometry: Geometry,
) {
this.onInteraction = (shapes: InteractionResult[] | null, shapesUpdated?: boolean, isDone?: boolean): void => {
this.shapesWereUpdated = false;
onInteraction(shapes, shapesUpdated, isDone);
};
this.canvas = canvas;
this.geometry = geometry;
this.shapesWereUpdated = false;
this.interactionShapes = [];
this.interactionData = { enabled: false };
this.currentInteractionShape = null;
this.crosshair = new Crosshair();
this.cursorPosition = {
x: 0,
y: 0,
};
this.canvas.on('mousemove.interaction', (e: MouseEvent): void => {
const [x, y] = translateToSVG((this.canvas.node as any) as SVGSVGElement, [e.clientX, e.clientY]);
this.cursorPosition = { x, y };
if (this.crosshair) {
this.crosshair.move(x, y);
}
});
document.body.addEventListener('keyup', (e: KeyboardEvent): void => {
if (e.keyCode === 17 && this.shouldRaiseEvent(false)) {
// 17 is ctrl
this.onInteraction(this.prepareResult(), true, false);
}
});
}
public transform(geometry: Geometry): void {
this.geometry = geometry;
if (this.crosshair) {
this.crosshair.scale(this.geometry.scale);
}
const shapesToBeScaled = this.currentInteractionShape
? [...this.interactionShapes, this.currentInteractionShape]
: [...this.interactionShapes];
for (const shape of shapesToBeScaled) {
if (shape.type === 'circle') {
(shape as SVG.Circle).radius(consts.BASE_POINT_SIZE / this.geometry.scale);
shape.attr('stroke-width', consts.POINTS_STROKE_WIDTH / this.geometry.scale);
} else {
shape.attr('stroke-width', consts.BASE_STROKE_WIDTH / this.geometry.scale);
}
}
}
public interact(interactionData: InteractionData): void {
if (interactionData.enabled) {
this.interactionData = interactionData;
this.initInteraction();
this.startInteraction();
} else {
this.onInteraction(this.prepareResult(), this.shouldRaiseEvent(false), true);
this.release();
this.interactionData = interactionData;
}
}
public cancel(): void {
this.release();
this.onInteraction(null);
}
}

@ -12,7 +12,6 @@ export interface MergeHandler {
repeatSelection(): void; repeatSelection(): void;
} }
export class MergeHandlerImpl implements MergeHandler { export class MergeHandlerImpl implements MergeHandler {
// callback is used to notify about merging end // callback is used to notify about merging end
private onMergeDone: (objects: any[] | null, duration?: number) => void; private onMergeDone: (objects: any[] | null, duration?: number) => void;
@ -40,8 +39,10 @@ export class MergeHandlerImpl implements MergeHandler {
} }
private checkConstraints(state: any): boolean { private checkConstraints(state: any): boolean {
return !this.constraints || (state.label.id === this.constraints.labelID return (
&& state.shapeType === this.constraints.shapeType); !this.constraints ||
(state.label.id === this.constraints.labelID && state.shapeType === this.constraints.shapeType)
);
} }
private release(): void { private release(): void {
@ -118,8 +119,7 @@ export class MergeHandlerImpl implements MergeHandler {
} }
} else { } else {
const shape = this.canvas.select(`#cvat_canvas_shape_${objectState.clientID}`).first(); const shape = this.canvas.select(`#cvat_canvas_shape_${objectState.clientID}`).first();
if (shape && this.checkConstraints(objectState) if (shape && this.checkConstraints(objectState) && !stateFrames.includes(objectState.frame)) {
&& !stateFrames.includes(objectState.frame)) {
this.statesToBeMerged.push(objectState); this.statesToBeMerged.push(objectState);
this.highlightedShapes[objectState.clientID] = shape; this.highlightedShapes[objectState.clientID] = shape;
shape.addClass('cvat_canvas_shape_merging'); shape.addClass('cvat_canvas_shape_merging');

@ -0,0 +1,133 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import * as SVG from 'svg.js';
import consts from './consts';
import { translateToSVG } from './shared';
import { Geometry } from './canvasModel';
export interface RegionSelector {
select(enabled: boolean): void;
cancel(): void;
transform(geometry: Geometry): void;
}
export class RegionSelectorImpl implements RegionSelector {
private onRegionSelected: (points?: number[]) => void;
private geometry: Geometry;
private canvas: SVG.Container;
private selectionRect: SVG.Rect | null;
private startSelectionPoint: {
x: number;
y: number;
};
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 onMouseMove = (event: MouseEvent): void => {
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,
});
}
};
private onMouseDown = (event: MouseEvent): void => {
if (!this.selectionRect && !event.altKey) {
const point = translateToSVG((this.canvas.node as any) as SVGSVGElement, [event.clientX, event.clientY]);
this.startSelectionPoint = {
x: point[0],
y: point[1],
};
this.selectionRect = this.canvas
.rect()
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
})
.addClass('cvat_canvas_shape_region_selection');
this.selectionRect.attr({ ...this.startSelectionPoint });
}
};
private onMouseUp = (): void => {
const { offset } = this.geometry;
if (this.selectionRect) {
const {
w, h, x, y, x2, y2,
} = this.selectionRect.bbox();
this.selectionRect.remove();
this.selectionRect = null;
if (w === 0 && h === 0) {
this.onRegionSelected([x - offset, y - offset]);
} else {
this.onRegionSelected([x - offset, y - offset, x2 - offset, y2 - offset]);
}
}
};
private startSelection(): void {
this.canvas.node.addEventListener('mousemove', this.onMouseMove);
this.canvas.node.addEventListener('mousedown', this.onMouseDown);
this.canvas.node.addEventListener('mouseup', this.onMouseUp);
}
private stopSelection(): void {
this.canvas.node.removeEventListener('mousemove', this.onMouseMove);
this.canvas.node.removeEventListener('mousedown', this.onMouseDown);
this.canvas.node.removeEventListener('mouseup', this.onMouseUp);
}
private release(): void {
this.stopSelection();
}
public constructor(onRegionSelected: (points?: number[]) => void, canvas: SVG.Container, geometry: Geometry) {
this.onRegionSelected = onRegionSelected;
this.geometry = geometry;
this.canvas = canvas;
this.selectionRect = null;
}
public select(enabled: boolean): void {
if (enabled) {
this.startSelection();
} else {
this.release();
}
}
public cancel(): void {
this.release();
this.onRegionSelected();
}
public transform(geometry: Geometry): void {
this.geometry = geometry;
if (this.selectionRect) {
this.selectionRect.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
});
}
}
}

@ -83,22 +83,25 @@ export function translateToSVG(svg: SVGSVGElement, points: number[]): number[] {
return output; return output;
} }
export function displayShapeSize( export function displayShapeSize(shapesContainer: SVG.Container, textContainer: SVG.Container): ShapeSizeElement {
shapesContainer: SVG.Container,
textContainer: SVG.Container,
): ShapeSizeElement {
const shapeSize: ShapeSizeElement = { const shapeSize: ShapeSizeElement = {
sizeElement: textContainer.text('').font({ sizeElement: textContainer
weight: 'bolder', .text('')
}).fill('white').addClass('cvat_canvas_text'), .font({
update(shape: SVG.Shape): void{ weight: 'bolder',
})
.fill('white')
.addClass('cvat_canvas_text'),
update(shape: SVG.Shape): void {
const bbox = shape.bbox(); const bbox = shape.bbox();
const text = `${bbox.width.toFixed(1)}x${bbox.height.toFixed(1)}`; const text = `${bbox.width.toFixed(1)}x${bbox.height.toFixed(1)}`;
const [x, y]: number[] = translateToSVG( const [x, y]: number[] = translateToSVG(
textContainer.node as any as SVGSVGElement, (textContainer.node as any) as SVGSVGElement,
translateFromSVG((shapesContainer.node as any as SVGSVGElement), [bbox.x, bbox.y]), translateFromSVG((shapesContainer.node as any) as SVGSVGElement, [bbox.x, bbox.y]),
); );
this.sizeElement.clear().plain(text) this.sizeElement
.clear()
.plain(text)
.move(x + consts.TEXT_MARGIN, y + consts.TEXT_MARGIN); .move(x + consts.TEXT_MARGIN, y + consts.TEXT_MARGIN);
}, },
rm(): void { rm(): void {
@ -120,7 +123,9 @@ export function pointsToNumberArray(points: string | Point[]): number[] {
}, []); }, []);
} }
return points.trim().split(/[,\s]+/g) return points
.trim()
.split(/[,\s]+/g)
.map((coord: string): number => +coord); .map((coord: string): number => +coord);
} }
@ -138,14 +143,19 @@ export function parsePoints(source: string | number[]): Point[] {
}, []); }, []);
} }
return source.trim().split(/\s/).map((point: string): Point => { return source
const [x, y] = point.split(',').map((coord: string): number => +coord); .trim()
return { x, y }; .split(/\s/)
}); .map(
(point: string): Point => {
const [x, y] = point.split(',').map((coord: string): number => +coord);
return { x, y };
},
);
} }
export function stringifyPoints(points: (Point | number)[]): string { export function stringifyPoints(points: (Point | number)[]): string {
if (typeof (points[0]) === 'number') { if (typeof points[0] === 'number') {
return points.reduce((acc: string, val: number, idx: number): string => { return points.reduce((acc: string, val: number, idx: number): string => {
if (idx % 2) { if (idx % 2) {
return `${acc},${val}`; return `${acc},${val}`;
@ -166,5 +176,5 @@ export function scalarProduct(a: Vector2D, b: Vector2D): number {
} }
export function vectorLength(vector: Vector2D): number { export function vectorLength(vector: Vector2D): number {
return Math.sqrt((vector.i ** 2) + (vector.j ** 2)); return Math.sqrt(vector.i ** 2 + vector.j ** 2);
} }

@ -85,12 +85,16 @@ export class SplitHandlerImpl implements SplitHandler {
this.highlightedShape = shape; this.highlightedShape = shape;
this.highlightedShape.addClass('cvat_canvas_shape_splitting'); this.highlightedShape.addClass('cvat_canvas_shape_splitting');
this.canvas.node.append(this.highlightedShape.node); this.canvas.node.append(this.highlightedShape.node);
this.highlightedShape.on('click.split', (): void => { this.highlightedShape.on(
this.splitDone = true; 'click.split',
this.onSplitDone(state); (): void => {
}, { this.splitDone = true;
once: true, this.onSplitDone(state);
}); },
{
once: true,
},
);
} }
} }
} }

File diff suppressed because it is too large Load Diff

@ -5,14 +5,9 @@
import * as SVG from 'svg.js'; import * as SVG from 'svg.js';
import consts from './consts'; import consts from './consts';
import { import { translateToSVG } from './shared';
translateToSVG,
} from './shared';
import {
Geometry,
} from './canvasModel';
import { Geometry } from './canvasModel';
export interface ZoomHandler { export interface ZoomHandler {
zoom(): void; zoom(): void;
@ -35,10 +30,7 @@ export class ZoomHandlerImpl implements ZoomHandler {
private onSelectStart(event: MouseEvent): void { private onSelectStart(event: MouseEvent): void {
if (!this.selectionRect && event.which === 1) { if (!this.selectionRect && event.which === 1) {
const point = translateToSVG( const point = translateToSVG((this.canvas.node as any) as SVGSVGElement, [event.clientX, event.clientY]);
(this.canvas.node as any as SVGSVGElement),
[event.clientX, event.clientY],
);
this.startSelectionPoint = { this.startSelectionPoint = {
x: point[0], x: point[0],
y: point[1], y: point[1],
@ -52,16 +44,15 @@ export class ZoomHandlerImpl implements ZoomHandler {
} }
} }
private getSelectionBox(event: MouseEvent): { private getSelectionBox(
event: MouseEvent,
): {
x: number; x: number;
y: number; y: number;
width: number; width: number;
height: number; height: number;
} { } {
const point = translateToSVG( const point = translateToSVG((this.canvas.node as any) as SVGSVGElement, [event.clientX, event.clientY]);
(this.canvas.node as any as SVGSVGElement),
[event.clientX, event.clientY],
);
const stopSelectionPoint = { const stopSelectionPoint = {
x: point[0], x: point[0],
y: point[1], y: point[1],

@ -1,21 +1,19 @@
{ {
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"emitDeclarationOnly": true, "emitDeclarationOnly": true,
"module": "es6", "module": "es6",
"target": "es6", "target": "es6",
"noImplicitAny": true, "noImplicitAny": true,
"preserveConstEnums": true, "preserveConstEnums": true,
"declaration": true, "declaration": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"esModuleInterop": true, "esModuleInterop": true,
"moduleResolution": "node", "moduleResolution": "node",
"declarationDir": "dist/declaration", "declarationDir": "dist/declaration",
"paths": { "paths": {
"cvat-canvas.node": ["dist/cvat-canvas.node"] "cvat-canvas.node": ["dist/cvat-canvas.node"]
} }
}, },
"include": [ "include": ["src/typescript/*.ts"]
"src/typescript/*.ts"
]
} }

@ -1,11 +1,12 @@
/* // Copyright (C) 2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
/* eslint-disable */ // eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require('path'); const path = require('path');
const DtsBundleWebpack = require('dts-bundle-webpack')
// eslint-disable-next-line @typescript-eslint/no-var-requires
const DtsBundleWebpack = require('dts-bundle-webpack');
const nodeConfig = { const nodeConfig = {
target: 'node', target: 'node',
@ -22,30 +23,38 @@ const nodeConfig = {
extensions: ['.ts', '.js', '.json'], extensions: ['.ts', '.js', '.json'],
}, },
module: { module: {
rules: [{ rules: [
test: /\.ts$/, {
exclude: /node_modules/, test: /\.ts$/,
use: { exclude: /node_modules/,
loader: 'babel-loader', use: {
options: { loader: 'babel-loader',
plugins: ['@babel/plugin-proposal-class-properties'], options: {
presets: [ plugins: [
['@babel/preset-env'], '@babel/plugin-proposal-class-properties',
['@babel/typescript'], '@babel/plugin-proposal-optional-chaining',
], ],
sourceType: 'unambiguous', presets: [['@babel/preset-env'], ['@babel/typescript']],
sourceType: 'unambiguous',
},
}, },
}, },
}, { {
test: /\.(css|scss)$/, test: /\.(css|scss)$/,
exclude: /node_modules/, exclude: /node_modules/,
use: ['style-loader', { use: [
loader: 'css-loader', 'style-loader',
options: { {
importLoaders: 2, loader: 'css-loader',
}, options: {
}, 'postcss-loader', 'sass-loader'] importLoaders: 2,
}], },
},
'postcss-loader',
'sass-loader',
],
},
],
}, },
plugins: [ plugins: [
new DtsBundleWebpack({ new DtsBundleWebpack({
@ -53,7 +62,7 @@ const nodeConfig = {
main: 'dist/declaration/src/typescript/canvas.d.ts', main: 'dist/declaration/src/typescript/canvas.d.ts',
out: '../cvat-canvas.node.d.ts', out: '../cvat-canvas.node.d.ts',
}), }),
] ],
}; };
const webConfig = { const webConfig = {
@ -79,32 +88,43 @@ const webConfig = {
extensions: ['.ts', '.js', '.json'], extensions: ['.ts', '.js', '.json'],
}, },
module: { module: {
rules: [{ rules: [
test: /\.ts$/, {
exclude: /node_modules/, test: /\.ts$/,
use: { exclude: /node_modules/,
loader: 'babel-loader', use: {
options: { loader: 'babel-loader',
plugins: ['@babel/plugin-proposal-class-properties'], options: {
presets: [ plugins: ['@babel/plugin-proposal-class-properties'],
['@babel/preset-env', { presets: [
targets: '> 2.5%', // https://github.com/browserslist/browserslist [
}], '@babel/preset-env',
['@babel/typescript'], {
], targets: '> 2.5%', // https://github.com/browserslist/browserslist
sourceType: 'unambiguous', },
],
['@babel/typescript'],
],
sourceType: 'unambiguous',
},
}, },
}, },
}, { {
test: /\.scss$/, test: /\.scss$/,
exclude: /node_modules/, exclude: /node_modules/,
use: ['style-loader', { use: [
loader: 'css-loader', 'style-loader',
options: { {
importLoaders: 2, loader: 'css-loader',
}, options: {
}, 'postcss-loader', 'sass-loader'] importLoaders: 2,
}], },
},
'postcss-loader',
'sass-loader',
],
},
],
}, },
plugins: [ plugins: [
new DtsBundleWebpack({ new DtsBundleWebpack({
@ -112,7 +132,7 @@ const webConfig = {
main: 'dist/declaration/src/typescript/canvas.d.ts', main: 'dist/declaration/src/typescript/canvas.d.ts',
out: '../cvat-canvas.d.ts', out: '../cvat-canvas.d.ts',
}), }),
] ],
}; };
module.exports = [webConfig, nodeConfig] module.exports = [webConfig, nodeConfig];

@ -0,0 +1 @@
webpack.config.js

@ -1,55 +1,47 @@
/* // Copyright (C) 2018-2020 Intel Corporation
* Copyright (C) 2018 Intel Corporation //
* // SPDX-License-Identifier: MIT
* SPDX-License-Identifier: MIT
*/
module.exports = { module.exports = {
"env": { env: {
"node": false, node: true,
"browser": true, browser: true,
"es6": true, es6: true,
"jquery": true, 'jest/globals': true,
"qunit": true,
}, },
"parserOptions": { parserOptions: {
"parser": "babel-eslint", parser: 'babel-eslint',
"sourceType": "module", sourceType: 'module',
"ecmaVersion": 2018, ecmaVersion: 2018,
}, },
"plugins": [ plugins: ['security', 'jest', 'no-unsafe-innerhtml', 'no-unsanitized'],
"security", extends: ['eslint:recommended', 'plugin:security/recommended', 'plugin:no-unsanitized/DOM', 'airbnb-base'],
"no-unsanitized", rules: {
"no-unsafe-innerhtml", 'no-await-in-loop': [0],
], 'global-require': [0],
"extends": [ 'no-new': [0],
"eslint:recommended", 'class-methods-use-this': [0],
"plugin:security/recommended", 'no-restricted-properties': [
"plugin:no-unsanitized/DOM", 0,
"airbnb-base", {
], object: 'Math',
"rules": { property: 'pow',
"no-await-in-loop": [0], },
"global-require": [0], ],
"no-new": [0], 'no-plusplus': [0],
"class-methods-use-this": [0], 'no-param-reassign': [0],
"no-restricted-properties": [0, { 'no-underscore-dangle': ['error', { allowAfterThis: true }],
"object": "Math", 'no-restricted-syntax': [0, { selector: 'ForOfStatement' }],
"property": "pow", 'no-continue': [0],
}], 'no-unsafe-innerhtml/no-unsafe-innerhtml': 1,
"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. // This rule actual for user input data on the node.js environment mainly.
"security/detect-object-injection": 0, 'security/detect-object-injection': 0,
"indent": ["warn", 4], indent: ['warn', 4],
"no-useless-constructor": 0, 'no-useless-constructor': 0,
"func-names": [0], 'func-names': [0],
"valid-typeof": [0], 'valid-typeof': [0],
"no-console": [0], // this rule deprecates console.log, console.warn etc. because "it is not good in production code" 'no-console': [0],
"max-classes-per-file": [0], 'max-classes-per-file': [0],
'max-len': ['warn', { code: 120 }],
}, },
}; };

@ -1,40 +1,47 @@
# Module CVAT-CORE # Module CVAT-CORE
## Description ## Description
This CVAT module is a client-side JavaScipt library to management of objects, frames, logs, etc. This CVAT module is a client-side JavaScipt library to management of objects, frames, logs, etc.
It contains the core logic of the Computer Vision Annotation Tool. It contains the core logic of the Computer Vision Annotation Tool.
## Versioning ## Versioning
If you make changes in this package, please do following: If you make changes in this package, please do following:
- After not important changes (typos, backward compatible bug fixes, refactoring) do: ``npm version patch`` - After not important changes (typos, backward compatible bug fixes, refactoring) do: `npm version patch`
- After changing API (backward compatible new features) do: ``npm version minor`` - After changing API (backward compatible new features) do: `npm version minor`
- After changing API (changes that break backward compatibility) do: ``npm version major`` - After changing API (changes that break backward compatibility) do: `npm version major`
### Commands ### Commands
- Dependencies installation - Dependencies installation
```bash ```bash
npm install npm ci
``` ```
- Building the module from sources in the ```dist``` directory: - Building the module from sources in the `dist` directory:
```bash ```bash
npm run build npm run build
npm run build -- --mode=development # without a minification npm run build -- --mode=development # without a minification
``` ```
- Building the documentation in the ```docs``` directory: - Building the documentation in the `docs` directory:
```bash ```bash
npm run-script docs npm run-script docs
``` ```
- Running of tests: - Running of tests:
```bash ```bash
npm run-script test npm run-script test
``` ```
- Updating of a module version: - Updating of a module version:
```bash ```bash
npm version patch # updated after minor fixes 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 minor # updated after major changes which don't affect API compatibility with previous versions
@ -42,5 +49,6 @@ npm version major # updated after major changes which affect API compatibility
``` ```
Visual studio code configurations: Visual studio code configurations:
- cvat.js debug starts debugging with entrypoint api.js - cvat.js debug starts debugging with entrypoint api.js
- cvat.js test builds library and runs entrypoint tests.js - cvat.js test builds library and runs entrypoint tests.js

@ -1,32 +1,15 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
const { defaults } = require('jest-config'); const { defaults } = require('jest-config');
module.exports = { module.exports = {
coverageDirectory: 'reports/coverage', coverageDirectory: 'reports/coverage',
coverageReporters: ['lcov'], coverageReporters: ['lcov'],
moduleFileExtensions: [ moduleFileExtensions: [...defaults.moduleFileExtensions, 'ts', 'tsx'],
...defaults.moduleFileExtensions, reporters: ['default', ['jest-junit', { outputDirectory: 'reports/junit' }]],
'ts', testMatch: ['**/tests/**/*.js'],
'tsx', testPathIgnorePatterns: ['/node_modules/', '/tests/mocks/*'],
],
reporters: [
'default',
['jest-junit', { outputDirectory: 'reports/junit' }],
],
testMatch: [
'**/tests/**/*.js',
],
testPathIgnorePatterns: [
'/node_modules/',
'/tests/mocks/*',
],
automock: false, automock: false,
}; };

@ -1,8 +1,6 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
module.exports = { module.exports = {
plugins: [], plugins: [],

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{ {
"name": "cvat-core", "name": "cvat-core",
"version": "3.5.0", "version": "3.10.0",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration", "description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js", "main": "babel.config.js",
"scripts": { "scripts": {
@ -25,6 +25,7 @@
"eslint-plugin-no-unsafe-innerhtml": "^1.0.16", "eslint-plugin-no-unsafe-innerhtml": "^1.0.16",
"eslint-plugin-no-unsanitized": "^3.0.2", "eslint-plugin-no-unsanitized": "^3.0.2",
"eslint-plugin-security": "^1.4.0", "eslint-plugin-security": "^1.4.0",
"eslint-plugin-jest": "^24.1.0",
"jest": "^24.8.0", "jest": "^24.8.0",
"jest-junit": "^6.4.0", "jest-junit": "^6.4.0",
"jsdoc": "^3.6.4", "jsdoc": "^3.6.4",
@ -32,16 +33,17 @@
"webpack-cli": "^3.3.2" "webpack-cli": "^3.3.2"
}, },
"dependencies": { "dependencies": {
"axios": "^0.18.0", "axios": "^0.21.1",
"browser-or-node": "^1.2.1", "browser-or-node": "^1.2.1",
"cvat-data": "../cvat-data", "cvat-data": "../cvat-data",
"detect-browser": "^5.0.0", "detect-browser": "^5.2.0",
"error-stack-parser": "^2.0.2", "error-stack-parser": "^2.0.2",
"form-data": "^2.5.0", "form-data": "^2.5.0",
"jest-config": "^24.8.0", "jest-config": "^26.6.3",
"js-cookie": "^2.2.0", "js-cookie": "^2.2.0",
"jsonpath": "^1.0.2", "jsonpath": "^1.0.2",
"platform": "^1.3.5", "platform": "^1.3.5",
"quickhull": "^1.0.3",
"store": "^2.0.12", "store": "^2.0.12",
"worker-loader": "^2.0.0" "worker-loader": "^2.0.0"
} }

@ -1,14 +1,13 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
(() => { (() => {
/** /**
* Class representing an annotation loader * Class representing an annotation loader
* @memberof module:API.cvat.classes * @memberof module:API.cvat.classes
* @hideconstructor * @hideconstructor
*/ */
class Loader { class Loader {
constructor(initialData) { constructor(initialData) {
const data = { const data = {
@ -21,42 +20,42 @@
Object.defineProperties(this, { Object.defineProperties(this, {
name: { name: {
/** /**
* @name name * @name name
* @type {string} * @type {string}
* @memberof module:API.cvat.classes.Loader * @memberof module:API.cvat.classes.Loader
* @readonly * @readonly
* @instance * @instance
*/ */
get: () => data.name, get: () => data.name,
}, },
format: { format: {
/** /**
* @name format * @name format
* @type {string} * @type {string}
* @memberof module:API.cvat.classes.Loader * @memberof module:API.cvat.classes.Loader
* @readonly * @readonly
* @instance * @instance
*/ */
get: () => data.format, get: () => data.format,
}, },
version: { version: {
/** /**
* @name version * @name version
* @type {string} * @type {string}
* @memberof module:API.cvat.classes.Loader * @memberof module:API.cvat.classes.Loader
* @readonly * @readonly
* @instance * @instance
*/ */
get: () => data.version, get: () => data.version,
}, },
enabled: { enabled: {
/** /**
* @name enabled * @name enabled
* @type {string} * @type {string}
* @memberof module:API.cvat.classes.Loader * @memberof module:API.cvat.classes.Loader
* @readonly * @readonly
* @instance * @instance
*/ */
get: () => data.enabled, get: () => data.enabled,
}, },
}); });
@ -64,10 +63,10 @@
} }
/** /**
* Class representing an annotation dumper * Class representing an annotation dumper
* @memberof module:API.cvat.classes * @memberof module:API.cvat.classes
* @hideconstructor * @hideconstructor
*/ */
class Dumper { class Dumper {
constructor(initialData) { constructor(initialData) {
const data = { const data = {
@ -80,42 +79,42 @@
Object.defineProperties(this, { Object.defineProperties(this, {
name: { name: {
/** /**
* @name name * @name name
* @type {string} * @type {string}
* @memberof module:API.cvat.classes.Dumper * @memberof module:API.cvat.classes.Dumper
* @readonly * @readonly
* @instance * @instance
*/ */
get: () => data.name, get: () => data.name,
}, },
format: { format: {
/** /**
* @name format * @name format
* @type {string} * @type {string}
* @memberof module:API.cvat.classes.Dumper * @memberof module:API.cvat.classes.Dumper
* @readonly * @readonly
* @instance * @instance
*/ */
get: () => data.format, get: () => data.format,
}, },
version: { version: {
/** /**
* @name version * @name version
* @type {string} * @type {string}
* @memberof module:API.cvat.classes.Dumper * @memberof module:API.cvat.classes.Dumper
* @readonly * @readonly
* @instance * @instance
*/ */
get: () => data.version, get: () => data.version,
}, },
enabled: { enabled: {
/** /**
* @name enabled * @name enabled
* @type {string} * @type {string}
* @memberof module:API.cvat.classes.Loader * @memberof module:API.cvat.classes.Loader
* @readonly * @readonly
* @instance * @instance
*/ */
get: () => data.enabled, get: () => data.enabled,
}, },
}); });
@ -123,10 +122,10 @@
} }
/** /**
* Class representing an annotation format * Class representing an annotation format
* @memberof module:API.cvat.classes * @memberof module:API.cvat.classes
* @hideconstructor * @hideconstructor
*/ */
class AnnotationFormats { class AnnotationFormats {
constructor(initialData) { constructor(initialData) {
const data = { const data = {
@ -138,22 +137,22 @@
Object.defineProperties(this, { Object.defineProperties(this, {
loaders: { loaders: {
/** /**
* @name loaders * @name loaders
* @type {module:API.cvat.classes.Loader[]} * @type {module:API.cvat.classes.Loader[]}
* @memberof module:API.cvat.classes.AnnotationFormats * @memberof module:API.cvat.classes.AnnotationFormats
* @readonly * @readonly
* @instance * @instance
*/ */
get: () => [...data.importers], get: () => [...data.importers],
}, },
dumpers: { dumpers: {
/** /**
* @name dumpers * @name dumpers
* @type {module:API.cvat.classes.Dumper[]} * @type {module:API.cvat.classes.Dumper[]}
* @memberof module:API.cvat.classes.AnnotationFormats * @memberof module:API.cvat.classes.AnnotationFormats
* @readonly * @readonly
* @instance * @instance
*/ */
get: () => [...data.exporters], get: () => [...data.exporters],
}, },
}); });

@ -1,11 +1,6 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019-2020 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
(() => { (() => {
const { const {
@ -28,17 +23,10 @@
const { checkObjectType } = require('./common'); const { checkObjectType } = require('./common');
const Statistics = require('./statistics'); const Statistics = require('./statistics');
const { Label } = require('./labels'); const { Label } = require('./labels');
const { const { DataError, ArgumentError, ScriptingError } = require('./exceptions');
DataError,
ArgumentError,
ScriptingError,
} = require('./exceptions');
const { const {
HistoryActions, HistoryActions, ObjectShape, ObjectType, colors,
ObjectShape,
ObjectType,
colors,
} = require('./enums'); } = require('./enums');
const ObjectState = require('./object-state'); const ObjectState = require('./object-state');
@ -64,15 +52,12 @@
shapeModel = new CuboidShape(shapeData, clientID, color, injection); shapeModel = new CuboidShape(shapeData, clientID, color, injection);
break; break;
default: default:
throw new DataError( throw new DataError(`An unexpected type of shape "${type}"`);
`An unexpected type of shape "${type}"`,
);
} }
return shapeModel; return shapeModel;
} }
function trackFactory(trackData, clientID, injection) { function trackFactory(trackData, clientID, injection) {
if (trackData.shapes.length) { if (trackData.shapes.length) {
const { type } = trackData.shapes[0]; const { type } = trackData.shapes[0];
@ -96,9 +81,7 @@
trackModel = new CuboidTrack(trackData, clientID, color, injection); trackModel = new CuboidTrack(trackData, clientID, color, injection);
break; break;
default: default:
throw new DataError( throw new DataError(`An unexpected type of track "${type}"`);
`An unexpected type of track "${type}"`,
);
} }
return trackModel; return trackModel;
@ -185,18 +168,20 @@
export() { export() {
const data = { const data = {
tracks: this.tracks.filter((track) => !track.removed) tracks: this.tracks.filter((track) => !track.removed).map((track) => track.toJSON()),
.map((track) => track.toJSON()),
shapes: Object.values(this.shapes) shapes: Object.values(this.shapes)
.reduce((accumulator, value) => { .reduce((accumulator, value) => {
accumulator.push(...value); accumulator.push(...value);
return accumulator; return accumulator;
}, []).filter((shape) => !shape.removed) }, [])
.filter((shape) => !shape.removed)
.map((shape) => shape.toJSON()), .map((shape) => shape.toJSON()),
tags: Object.values(this.tags).reduce((accumulator, value) => { tags: Object.values(this.tags)
accumulator.push(...value); .reduce((accumulator, value) => {
return accumulator; accumulator.push(...value);
}, []).filter((tag) => !tag.removed) return accumulator;
}, [])
.filter((tag) => !tag.removed)
.map((tag) => tag.toJSON()), .map((tag) => tag.toJSON()),
}; };
@ -252,7 +237,7 @@
const objectsForMerge = objectStates.map((state) => { const objectsForMerge = objectStates.map((state) => {
checkObjectType('object state', state, null, ObjectState); checkObjectType('object state', state, null, ObjectState);
const object = this.objects[state.clientID]; const object = this.objects[state.clientID];
if (typeof (object) === 'undefined') { if (typeof object === 'undefined') {
throw new ArgumentError( throw new ArgumentError(
'The object has not been saved yet. Call ObjectState.put([state]) before you can merge it', 'The object has not been saved yet. Call ObjectState.put([state]) before you can merge it',
); );
@ -263,15 +248,11 @@
const keyframes = {}; // frame: position const keyframes = {}; // frame: position
const { label, shapeType } = objectStates[0]; const { label, shapeType } = objectStates[0];
if (!(label.id in this.labels)) { if (!(label.id in this.labels)) {
throw new ArgumentError( throw new ArgumentError(`Unknown label for the task: ${label.id}`);
`Unknown label for the task: ${label.id}`,
);
} }
if (!Object.values(ObjectShape).includes(shapeType)) { if (!Object.values(ObjectShape).includes(shapeType)) {
throw new ArgumentError( throw new ArgumentError(`Got unknown shapeType "${shapeType}"`);
`Got unknown shapeType "${shapeType}"`,
);
} }
const labelAttributes = label.attributes.reduce((accumulator, attribute) => { const labelAttributes = label.attributes.reduce((accumulator, attribute) => {
@ -290,18 +271,14 @@
} }
if (state.shapeType !== shapeType) { if (state.shapeType !== shapeType) {
throw new ArgumentError( throw new ArgumentError(`All shapes are expected to be ${shapeType}, but got ${state.shapeType}`);
`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 this object is shape, get it position and save as a keyframe
if (object instanceof Shape) { if (object instanceof Shape) {
// Frame already saved and it is not outside // Frame already saved and it is not outside
if (object.frame in keyframes && !keyframes[object.frame].outside) { if (object.frame in keyframes && !keyframes[object.frame].outside) {
throw new ArgumentError( throw new ArgumentError('Expected only one visible shape per frame');
'Expected only one visible shape per frame',
);
} }
keyframes[object.frame] = { keyframes[object.frame] = {
@ -325,9 +302,8 @@
// Push outside shape after each annotation shape // Push outside shape after each annotation shape
// Any not outside shape rewrites it // Any not outside shape rewrites it
if (!((object.frame + 1) in keyframes) && object.frame + 1 <= this.stopFrame) { if (!(object.frame + 1 in keyframes) && object.frame + 1 <= this.stopFrame) {
keyframes[object.frame + 1] = JSON keyframes[object.frame + 1] = JSON.parse(JSON.stringify(keyframes[object.frame]));
.parse(JSON.stringify(keyframes[object.frame]));
keyframes[object.frame + 1].outside = true; keyframes[object.frame + 1].outside = true;
keyframes[object.frame + 1].frame++; keyframes[object.frame + 1].frame++;
} }
@ -344,17 +320,14 @@
continue; continue;
} }
throw new ArgumentError( throw new ArgumentError('Expected only one visible shape per frame');
'Expected only one visible shape per frame',
);
} }
// We do not save an attribute if it has the same value // We do not save an attribute if it has the same value
// We save only updates // We save only updates
let updatedAttributes = false; let updatedAttributes = false;
for (const attrID in shape.attributes) { for (const attrID in shape.attributes) {
if (!(attrID in attributes) if (!(attrID in attributes) || attributes[attrID] !== shape.attributes[attrID]) {
|| attributes[attrID] !== shape.attributes[attrID]) {
updatedAttributes = true; updatedAttributes = true;
attributes[attrID] = shape.attributes[attrID]; attributes[attrID] = shape.attributes[attrID];
} }
@ -367,15 +340,16 @@
occluded: shape.occluded, occluded: shape.occluded,
outside: shape.outside, outside: shape.outside,
zOrder: shape.zOrder, zOrder: shape.zOrder,
attributes: updatedAttributes ? Object.keys(attributes) attributes: updatedAttributes
.reduce((accumulator, attrID) => { ? Object.keys(attributes).reduce((accumulator, attrID) => {
accumulator.push({ accumulator.push({
spec_id: +attrID, spec_id: +attrID,
value: attributes[attrID], value: attributes[attrID],
}); });
return accumulator; return accumulator;
}, []) : [], }, [])
: [],
}; };
} }
} else { } else {
@ -399,22 +373,24 @@
const clientID = ++this.count; const clientID = ++this.count;
const track = { const track = {
frame: Math.min.apply(null, Object.keys(keyframes).map((frame) => +frame)), frame: Math.min.apply(
null,
Object.keys(keyframes).map((frame) => +frame),
),
shapes: Object.values(keyframes), shapes: Object.values(keyframes),
group: 0, group: 0,
source: objectStates[0].source, source: objectStates[0].source,
label_id: label.id, label_id: label.id,
attributes: Object.keys(objectStates[0].attributes) attributes: Object.keys(objectStates[0].attributes).reduce((accumulator, attrID) => {
.reduce((accumulator, attrID) => { if (!labelAttributes[attrID].mutable) {
if (!labelAttributes[attrID].mutable) { accumulator.push({
accumulator.push({ spec_id: +attrID,
spec_id: +attrID, value: objectStates[0].attributes[attrID],
value: objectStates[0].attributes[attrID], });
}); }
}
return accumulator; return accumulator;
}, []), }, []),
}; };
const trackModel = trackFactory(track, clientID, this.injection); const trackModel = trackFactory(track, clientID, this.injection);
@ -426,20 +402,23 @@
object.removed = true; object.removed = true;
} }
this.history.do(HistoryActions.MERGED_OBJECTS, () => { this.history.do(
trackModel.removed = true; HistoryActions.MERGED_OBJECTS,
for (const object of objectsForMerge) { () => {
object.removed = false; trackModel.removed = true;
} for (const object of objectsForMerge) {
}, () => { object.removed = false;
trackModel.removed = false; }
for (const object of objectsForMerge) { },
object.removed = true; () => {
} trackModel.removed = false;
}, [ for (const object of objectsForMerge) {
...objectsForMerge object.removed = true;
.map((object) => object.clientID), trackModel.clientID, }
], objectStates[0].frame); },
[...objectsForMerge.map((object) => object.clientID), trackModel.clientID],
objectStates[0].frame,
);
} }
split(objectState, frame) { split(objectState, frame) {
@ -447,10 +426,8 @@
checkObjectType('frame', frame, 'integer', null); checkObjectType('frame', frame, 'integer', null);
const object = this.objects[objectState.clientID]; const object = this.objects[objectState.clientID];
if (typeof (object) === 'undefined') { if (typeof object === 'undefined') {
throw new ArgumentError( throw new ArgumentError('The object has not been saved yet. Call annotations.put([state]) before');
'The object has not been saved yet. Call annotations.put([state]) before',
);
} }
if (objectState.objectType !== ObjectType.TRACK) { if (objectState.objectType !== ObjectType.TRACK) {
@ -474,17 +451,16 @@
occluded: objectState.occluded, occluded: objectState.occluded,
outside: objectState.outside, outside: objectState.outside,
zOrder: objectState.zOrder, zOrder: objectState.zOrder,
attributes: Object.keys(objectState.attributes) attributes: Object.keys(objectState.attributes).reduce((accumulator, attrID) => {
.reduce((accumulator, attrID) => { if (!labelAttributes[attrID].mutable) {
if (!labelAttributes[attrID].mutable) { accumulator.push({
accumulator.push({ spec_id: +attrID,
spec_id: +attrID, value: objectState.attributes[attrID],
value: objectState.attributes[attrID], });
}); }
}
return accumulator; return accumulator;
}, []), }, []),
frame, frame,
}; };
@ -526,15 +502,21 @@
// Remove source object // Remove source object
object.removed = true; object.removed = true;
this.history.do(HistoryActions.SPLITTED_TRACK, () => { this.history.do(
object.removed = false; HistoryActions.SPLITTED_TRACK,
prevTrack.removed = true; () => {
nextTrack.removed = true; object.removed = false;
}, () => { prevTrack.removed = true;
object.removed = true; nextTrack.removed = true;
prevTrack.removed = false; },
nextTrack.removed = false; () => {
}, [object.clientID, prevTrack.clientID, nextTrack.clientID], frame); object.removed = true;
prevTrack.removed = false;
nextTrack.removed = false;
},
[object.clientID, prevTrack.clientID, nextTrack.clientID],
frame,
);
} }
group(objectStates, reset) { group(objectStates, reset) {
@ -543,10 +525,8 @@
const objectsForGroup = objectStates.map((state) => { const objectsForGroup = objectStates.map((state) => {
checkObjectType('object state', state, null, ObjectState); checkObjectType('object state', state, null, ObjectState);
const object = this.objects[state.clientID]; const object = this.objects[state.clientID];
if (typeof (object) === 'undefined') { if (typeof object === 'undefined') {
throw new ArgumentError( throw new ArgumentError('The object has not been saved yet. Call annotations.put([state]) before');
'The object has not been saved yet. Call annotations.put([state]) before',
);
} }
return object; return object;
}); });
@ -558,15 +538,21 @@
} }
const redoGroups = objectsForGroup.map((object) => object.group); const redoGroups = objectsForGroup.map((object) => object.group);
this.history.do(HistoryActions.GROUPED_OBJECTS, () => { this.history.do(
objectsForGroup.forEach((object, idx) => { HistoryActions.GROUPED_OBJECTS,
object.group = undoGroups[idx]; () => {
}); objectsForGroup.forEach((object, idx) => {
}, () => { object.group = undoGroups[idx];
objectsForGroup.forEach((object, idx) => { });
object.group = redoGroups[idx]; },
}); () => {
}, objectsForGroup.map((object) => object.clientID), objectStates[0].frame); objectsForGroup.forEach((object, idx) => {
object.group = redoGroups[idx];
});
},
objectsForGroup.map((object) => object.clientID),
objectStates[0].frame,
);
return groupIdx; return groupIdx;
} }
@ -629,9 +615,7 @@
} else if (object instanceof Tag) { } else if (object instanceof Tag) {
objectType = 'tag'; objectType = 'tag';
} else { } else {
throw new ScriptingError( throw new ScriptingError(`Unexpected object type: "${objectType}"`);
`Unexpected object type: "${objectType}"`,
);
} }
const label = object.label.name; const label = object.label.name;
@ -645,7 +629,8 @@
if (objectType === 'track') { if (objectType === 'track') {
const keyframes = Object.keys(object.shapes) const keyframes = Object.keys(object.shapes)
.sort((a, b) => +a - +b).map((el) => +el); .sort((a, b) => +a - +b)
.map((el) => +el);
let prevKeyframe = keyframes[0]; let prevKeyframe = keyframes[0];
let visible = false; let visible = false;
@ -680,7 +665,7 @@
for (const label of Object.keys(labels)) { for (const label of Object.keys(labels)) {
for (const key of Object.keys(labels[label])) { for (const key of Object.keys(labels[label])) {
if (typeof (labels[label][key]) === 'object') { if (typeof labels[label][key] === 'object') {
for (const objectType of Object.keys(labels[label][key])) { for (const objectType of Object.keys(labels[label][key])) {
total[key][objectType] += labels[label][key][objectType]; total[key][objectType] += labels[label][key][objectType];
} }
@ -723,8 +708,7 @@
checkObjectType('state attributes', state.attributes, null, Object); checkObjectType('state attributes', state.attributes, null, Object);
checkObjectType('state label', state.label, null, Label); checkObjectType('state label', state.label, null, Label);
const attributes = Object.keys(state.attributes) const attributes = Object.keys(state.attributes).reduce(convertAttributes.bind(state), []);
.reduce(convertAttributes.bind(state), []);
const labelAttributes = state.label.attributes.reduce((accumulator, attribute) => { const labelAttributes = state.label.attributes.reduce((accumulator, attribute) => {
accumulator[attribute.id] = attribute; accumulator[attribute.id] = attribute;
return accumulator; return accumulator;
@ -749,8 +733,7 @@
if (!Object.values(ObjectShape).includes(state.shapeType)) { if (!Object.values(ObjectShape).includes(state.shapeType)) {
throw new ArgumentError( throw new ArgumentError(
'Object shape must be one of: ' `Object shape must be one of: ${JSON.stringify(Object.values(ObjectShape))}`,
+ `${JSON.stringify(Object.values(ObjectShape))}`,
); );
} }
@ -768,27 +751,26 @@
}); });
} else if (state.objectType === 'track') { } else if (state.objectType === 'track') {
constructed.tracks.push({ constructed.tracks.push({
attributes: attributes attributes: attributes.filter((attr) => !labelAttributes[attr.spec_id].mutable),
.filter((attr) => !labelAttributes[attr.spec_id].mutable),
frame: state.frame, frame: state.frame,
group: 0, group: 0,
source: state.source, source: state.source,
label_id: state.label.id, label_id: state.label.id,
shapes: [{ shapes: [
attributes: attributes {
.filter((attr) => labelAttributes[attr.spec_id].mutable), attributes: attributes.filter((attr) => labelAttributes[attr.spec_id].mutable),
frame: state.frame, frame: state.frame,
occluded: state.occluded || false, occluded: state.occluded || false,
outside: false, outside: false,
points: [...state.points], points: [...state.points],
type: state.shapeType, type: state.shapeType,
z_order: state.zOrder, z_order: state.zOrder,
}], },
],
}); });
} else { } else {
throw new ArgumentError( throw new ArgumentError(
'Object type must be one of: ' `Object type must be one of: ${JSON.stringify(Object.values(ObjectType))}`,
+ `${JSON.stringify(Object.values(ObjectType))}`,
); );
} }
} }
@ -796,20 +778,24 @@
// Add constructed objects to a collection // Add constructed objects to a collection
const imported = this.import(constructed); const imported = this.import(constructed);
const importedArray = imported.tags const importedArray = imported.tags.concat(imported.tracks).concat(imported.shapes);
.concat(imported.tracks)
.concat(imported.shapes);
if (objectStates.length) { if (objectStates.length) {
this.history.do(HistoryActions.CREATED_OBJECTS, () => { this.history.do(
importedArray.forEach((object) => { HistoryActions.CREATED_OBJECTS,
object.removed = true; () => {
}); importedArray.forEach((object) => {
}, () => { object.removed = true;
importedArray.forEach((object) => { });
object.removed = false; },
}); () => {
}, importedArray.map((object) => object.clientID), objectStates[0].frame); importedArray.forEach((object) => {
object.removed = false;
});
},
importedArray.map((object) => object.clientID),
objectStates[0].frame,
);
} }
return importedArray.map((value) => value.clientID); return importedArray.map((value) => value.clientID);
@ -829,14 +815,11 @@
} }
const object = this.objects[state.clientID]; const object = this.objects[state.clientID];
if (typeof (object) === 'undefined') { if (typeof object === 'undefined') {
throw new ArgumentError( throw new ArgumentError('The object has not been saved yet. Call annotations.put([state]) before');
'The object has not been saved yet. Call annotations.put([state]) before',
);
} }
const distance = object.constructor.distance(state.points, x, y); const distance = object.constructor.distance(state.points, x, y);
if (distance !== null && (minimumDistance === null if (distance !== null && (minimumDistance === null || distance < minimumDistance)) {
|| distance < minimumDistance)) {
minimumDistance = distance; minimumDistance = distance;
minimumState = state; minimumState = state;
} }
@ -848,14 +831,47 @@
}; };
} }
searchEmpty(frameFrom, frameTo) {
const sign = Math.sign(frameTo - frameFrom);
const predicate = sign > 0 ? (frame) => frame <= frameTo : (frame) => frame >= frameTo;
const update = sign > 0 ? (frame) => frame + 1 : (frame) => frame - 1;
for (let frame = frameFrom; predicate(frame); frame = update(frame)) {
if (frame in this.shapes && this.shapes[frame].some((shape) => !shape.removed)) {
continue;
}
if (frame in this.tags && this.tags[frame].some((tag) => !tag.removed)) {
continue;
}
const filteredTracks = this.tracks.filter((track) => !track.removed);
let found = false;
for (const track of filteredTracks) {
const keyframes = track.boundedKeyframes(frame);
const { prev, first } = keyframes;
const last = prev === null ? first : prev;
const lastShape = track.shapes[last];
const isKeyfame = frame in track.shapes;
if (first <= frame && (!lastShape.outside || isKeyfame)) {
found = true;
break;
}
}
if (found) continue;
return frame;
}
return null;
}
search(filters, frameFrom, frameTo) { search(filters, frameFrom, frameTo) {
const [groups, query] = this.annotationsFilter.toJSONQuery(filters); const [groups, query] = this.annotationsFilter.toJSONQuery(filters);
const sign = Math.sign(frameTo - frameFrom); const sign = Math.sign(frameTo - frameFrom);
const flattenedQuery = groups.flat(Number.MAX_SAFE_INTEGER); const flattenedQuery = groups.flat(Number.MAX_SAFE_INTEGER);
const containsDifficultProperties = flattenedQuery const containsDifficultProperties = flattenedQuery.some(
.some((fragment) => fragment (fragment) => fragment.match(/^width/) || fragment.match(/^height/),
.match(/^width/) || fragment.match(/^height/)); );
const deepSearch = (deepSearchFrom, deepSearchTo) => { const deepSearch = (deepSearchFrom, deepSearchTo) => {
// deepSearchFrom is expected to be a frame that doesn't satisfy a filter // deepSearchFrom is expected to be a frame that doesn't satisfy a filter
@ -878,12 +894,8 @@
}; };
const keyframesMemory = {}; const keyframesMemory = {};
const predicate = sign > 0 const predicate = sign > 0 ? (frame) => frame <= frameTo : (frame) => frame >= frameTo;
? (frame) => frame <= frameTo const update = sign > 0 ? (frame) => frame + 1 : (frame) => frame - 1;
: (frame) => frame >= frameTo;
const update = sign > 0
? (frame) => frame + 1
: (frame) => frame - 1;
for (let frame = frameFrom; predicate(frame); frame = update(frame)) { for (let frame = frameFrom; predicate(frame); frame = update(frame)) {
// First prepare all data for the frame // First prepare all data for the frame
// Consider all shapes, tags, and not outside tracks that have keyframe here // Consider all shapes, tags, and not outside tracks that have keyframe here
@ -897,15 +909,9 @@
.map((tag) => tag.get(frame)), .map((tag) => tag.get(frame)),
); );
const tracks = Object.values(this.tracks) const tracks = Object.values(this.tracks)
.filter((track) => ( .filter((track) => frame in track.shapes || frame === frameFrom || frame === frameTo)
frame in track.shapes .filter((track) => !track.removed);
|| frame === frameFrom statesData.push(...tracks.map((track) => track.get(frame)).filter((state) => !state.outside));
|| frame === frameTo
)).filter((track) => !track.removed);
statesData.push(
...tracks.map((track) => track.get(frame))
.filter((state) => !state.outside),
);
// Nothing to filtering, go to the next iteration // Nothing to filtering, go to the next iteration
if (!statesData.length) { if (!statesData.length) {
@ -926,12 +932,8 @@
for (const track of tracks) { for (const track of tracks) {
const trackIsSatisfy = filtered.includes(track.clientID); const trackIsSatisfy = filtered.includes(track.clientID);
if (!trackIsSatisfy) { if (!trackIsSatisfy) {
keyframesMemory[track.clientID] = [ keyframesMemory[track.clientID] = [filtered.includes(track.clientID), frame];
filtered.includes(track.clientID), } else if (keyframesMemory[track.clientID] && keyframesMemory[track.clientID][0] === false) {
frame,
];
} else if (keyframesMemory[track.clientID]
&& keyframesMemory[track.clientID][0] === false) {
withDeepSearch = true; withDeepSearch = true;
} }
} }
@ -939,9 +941,7 @@
if (withDeepSearch) { if (withDeepSearch) {
const reducer = sign > 0 ? Math.min : Math.max; const reducer = sign > 0 ? Math.min : Math.max;
const deepSearchFrom = reducer( const deepSearchFrom = reducer(...Object.values(keyframesMemory).map((value) => value[1]));
...Object.values(keyframesMemory).map((value) => value[1]),
);
return deepSearch(deepSearchFrom, frame); return deepSearch(deepSearchFrom, frame);
} }

@ -1,20 +1,11 @@
/* // Copyright (C) 2020 Intel Corporation
* Copyright (C) 2020 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
const jsonpath = require('jsonpath'); const jsonpath = require('jsonpath');
const { const { AttributeType, ObjectType } = require('./enums');
AttributeType,
ObjectType,
} = require('./enums');
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
class AnnotationsFilter { class AnnotationsFilter {
constructor() { constructor() {
// eslint-disable-next-line security/detect-unsafe-regex // eslint-disable-next-line security/detect-unsafe-regex
@ -47,8 +38,7 @@ class AnnotationsFilter {
if (operators.includes(expression[i])) { if (operators.includes(expression[i])) {
if (!nestedCounter) { if (!nestedCounter) {
const subexpression = expression const subexpression = expression.substr(start + 1, i - start - 1).trim();
.substr(start + 1, i - start - 1).trim();
splitted.push(subexpression); splitted.push(subexpression);
splitted.push(expression[i]); splitted.push(expression[i]);
start = i; start = i;
@ -56,18 +46,14 @@ class AnnotationsFilter {
} }
} }
const subexpression = expression const subexpression = expression.substr(start + 1).trim();
.substr(start + 1).trim();
splitted.push(subexpression); splitted.push(subexpression);
splitted.forEach((internalExpression) => { splitted.forEach((internalExpression) => {
if (internalExpression === '|' || internalExpression === '&') { if (internalExpression === '|' || internalExpression === '&') {
container.push(internalExpression); container.push(internalExpression);
} else { } else {
this._groupByBrackets( this._groupByBrackets(container, internalExpression);
container,
internalExpression,
);
} }
}); });
} }
@ -103,12 +89,8 @@ class AnnotationsFilter {
endBracket = i; endBracket = i;
const subcontainer = []; const subcontainer = [];
const subexpression = expression const subexpression = expression.substr(startBracket + 1, endBracket - 1 - startBracket);
.substr(startBracket + 1, endBracket - 1 - startBracket); this._splitWithOperator(subcontainer, subexpression);
this._splitWithOperator(
subcontainer,
subexpression,
);
container.push(subcontainer); container.push(subcontainer);
@ -136,7 +118,7 @@ class AnnotationsFilter {
for (const group of groups) { for (const group of groups) {
if (Array.isArray(group)) { if (Array.isArray(group)) {
expression += `(${this._join(group)})`; expression += `(${this._join(group)})`;
} else if (typeof (group) === 'string') { } else if (typeof group === 'string') {
// it can be operator or expression // it can be operator or expression
if (group === '|' || group === '&') { if (group === '|' || group === '&') {
expression += group; expression += group;
@ -158,11 +140,10 @@ class AnnotationsFilter {
_convertObjects(statesData) { _convertObjects(statesData) {
const objects = statesData.map((state) => { const objects = statesData.map((state) => {
const labelAttributes = state.label.attributes const labelAttributes = state.label.attributes.reduce((acc, attr) => {
.reduce((acc, attr) => { acc[attr.id] = attr;
acc[attr.id] = attr; return acc;
return acc; }, {});
}, {});
let xtl = Number.MAX_SAFE_INTEGER; let xtl = Number.MAX_SAFE_INTEGER;
let xbr = Number.MIN_SAFE_INTEGER; let xbr = Number.MIN_SAFE_INTEGER;
@ -172,10 +153,12 @@ class AnnotationsFilter {
if (state.objectType !== ObjectType.TAG) { if (state.objectType !== ObjectType.TAG) {
state.points.forEach((coord, idx) => { state.points.forEach((coord, idx) => {
if (idx % 2) { // y if (idx % 2) {
// y
ytl = Math.min(ytl, coord); ytl = Math.min(ytl, coord);
ybr = Math.max(ybr, coord); ybr = Math.max(ybr, coord);
} else { // x } else {
// x
xtl = Math.min(xtl, coord); xtl = Math.min(xtl, coord);
xbr = Math.max(xbr, coord); xbr = Math.max(xbr, coord);
} }
@ -216,7 +199,7 @@ class AnnotationsFilter {
toJSONQuery(filters) { toJSONQuery(filters) {
try { try {
if (!Array.isArray(filters) || filters.some((value) => typeof (value) !== 'string')) { if (!Array.isArray(filters) || filters.some((value) => typeof value !== 'string')) {
throw Error('Argument must be an array of strings'); throw Error('Argument must be an array of strings');
} }
@ -225,7 +208,10 @@ class AnnotationsFilter {
} }
const groups = []; const groups = [];
const expression = filters.map((filter) => `(${filter})`).join('|').replace(/\\"/g, '`'); const expression = filters
.map((filter) => `(${filter})`)
.join('|')
.replace(/\\"/g, '`');
this._splitWithOperator(groups, expression); this._splitWithOperator(groups, expression);
return [groups, `$.objects[?(${this._join(groups)})].clientID`]; return [groups, `$.objects[?(${this._join(groups)})].clientID`];
} catch (error) { } catch (error) {

@ -1,15 +1,19 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019-2020 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
const MAX_HISTORY_LENGTH = 128; const MAX_HISTORY_LENGTH = 128;
class AnnotationHistory { class AnnotationHistory {
constructor() { constructor() {
this.frozen = false;
this.clear(); this.clear();
} }
freeze(frozen) {
this.frozen = frozen;
}
get() { get() {
return { return {
undo: this._undo.map((undo) => [undo.action, undo.frame]), undo: this._undo.map((undo) => [undo.action, undo.frame]),
@ -18,6 +22,7 @@ class AnnotationHistory {
} }
do(action, undo, redo, clientIDs, frame) { do(action, undo, redo, clientIDs, frame) {
if (this.frozen) return;
const actionItem = { const actionItem = {
clientIDs, clientIDs,
action, action,

File diff suppressed because it is too large Load Diff

@ -1,16 +1,11 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
(() => { (() => {
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy');
const { Task } = require('./session'); const { Task } = require('./session');
const { ScriptingError } = ('./exceptions'); const { ScriptingError } = './exceptions';
class AnnotationsSaver { class AnnotationsSaver {
constructor(version, collection, session) { constructor(version, collection, session) {
@ -53,12 +48,7 @@
} }
async _request(data, action) { async _request(data, action) {
const result = await serverProxy.annotations.updateAnnotations( const result = await serverProxy.annotations.updateAnnotations(this.sessionType, this.id, data, action);
this.sessionType,
this.id,
data,
action,
);
return result; return result;
} }
@ -102,27 +92,37 @@
}, },
}; };
const keys = ['id', 'label_id', 'group', 'frame', const keys = [
'occluded', 'z_order', 'points', 'type', 'shapes', 'id',
'attributes', 'value', 'spec_id', 'source', 'outside']; 'label_id',
'group',
'frame',
'occluded',
'z_order',
'points',
'type',
'shapes',
'attributes',
'value',
'spec_id',
'source',
'outside',
];
// Find created and updated objects // Find created and updated objects
for (const type of Object.keys(exported)) { for (const type of Object.keys(exported)) {
for (const object of exported[type]) { for (const object of exported[type]) {
if (object.id in this.initialObjects[type]) { if (object.id in this.initialObjects[type]) {
const exportedHash = JSON.stringify(object, keys); const exportedHash = JSON.stringify(object, keys);
const initialHash = JSON.stringify( const initialHash = JSON.stringify(this.initialObjects[type][object.id], keys);
this.initialObjects[type][object.id], keys,
);
if (exportedHash !== initialHash) { if (exportedHash !== initialHash) {
splitted.updated[type].push(object); splitted.updated[type].push(object);
} }
} else if (typeof (object.id) === 'undefined') { } else if (typeof object.id === 'undefined') {
splitted.created[type].push(object); splitted.created[type].push(object);
} else { } else {
throw new ScriptingError( throw new ScriptingError(
`Id of object is defined "${object.id}"` `Id of object is defined "${object.id}" but it absents in initial state`,
+ 'but it absents in initial state',
); );
} }
} }
@ -144,21 +144,17 @@
} }
} }
return splitted; return splitted;
} }
_updateCreatedObjects(saved, indexes) { _updateCreatedObjects(saved, indexes) {
const savedLength = saved.tracks.length const savedLength = saved.tracks.length + saved.shapes.length + saved.tags.length;
+ saved.shapes.length + saved.tags.length;
const indexesLength = indexes.tracks.length const indexesLength = indexes.tracks.length + indexes.shapes.length + indexes.tags.length;
+ indexes.shapes.length + indexes.tags.length;
if (indexesLength !== savedLength) { if (indexesLength !== savedLength) {
throw new ScriptingError( throw new ScriptingError(
'Number of indexes is differed by number of saved objects' `Number of indexes is differed by number of saved objects ${indexesLength} vs ${savedLength}`,
+ `${indexesLength} vs ${savedLength}`,
); );
} }
@ -180,7 +176,9 @@
}; };
// Remove them from the request body // Remove them from the request body
exported.tracks.concat(exported.shapes).concat(exported.tags) exported.tracks
.concat(exported.shapes)
.concat(exported.tags)
.map((value) => { .map((value) => {
delete value.clientID; delete value.clientID;
return value; return value;
@ -214,11 +212,7 @@
} }
} }
} else { } else {
const { const { created, updated, deleted } = this._split(exported);
created,
updated,
deleted,
} = this._split(exported);
onUpdate('Created objects are being saved on the server'); onUpdate('Created objects are being saved on the server');
const indexes = this._receiveIndexes(created); const indexes = this._receiveIndexes(created);

@ -1,11 +1,6 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019-2020 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
(() => { (() => {
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy');
@ -14,15 +9,8 @@
const AnnotationsHistory = require('./annotations-history'); const AnnotationsHistory = require('./annotations-history');
const { checkObjectType } = require('./common'); const { checkObjectType } = require('./common');
const { Task } = require('./session'); const { Task } = require('./session');
const { const { Loader, Dumper } = require('./annotation-formats');
Loader, const { ScriptingError, DataError, ArgumentError } = require('./exceptions');
Dumper,
} = require('./annotation-formats.js');
const {
ScriptingError,
DataError,
ArgumentError,
} = require('./exceptions');
const jobCache = new WeakMap(); const jobCache = new WeakMap();
const taskCache = new WeakMap(); const taskCache = new WeakMap();
@ -36,9 +24,7 @@
return jobCache; return jobCache;
} }
throw new ScriptingError( throw new ScriptingError(`Unknown session type was received ${sessionType}`);
`Unknown session type was received ${sessionType}`,
);
} }
async function getAnnotationsFromServer(session) { async function getAnnotationsFromServer(session) {
@ -46,8 +32,7 @@
const cache = getCache(sessionType); const cache = getCache(sessionType);
if (!cache.has(session)) { if (!cache.has(session)) {
const rawAnnotations = await serverProxy.annotations const rawAnnotations = await serverProxy.annotations.getAnnotations(sessionType, session.id);
.getAnnotations(sessionType, session.id);
// Get meta information about frames // Get meta information about frames
const startFrame = sessionType === 'job' ? session.startFrame : 0; const startFrame = sessionType === 'job' ? session.startFrame : 0;
@ -122,6 +107,19 @@
); );
} }
function searchEmptyFrame(session, frameFrom, frameTo) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).collection.searchEmpty(frameFrom, frameTo);
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
function mergeAnnotations(session, objectStates) { function mergeAnnotations(session, objectStates) {
const sessionType = session instanceof Task ? 'task' : 'job'; const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType); const cache = getCache(sessionType);
@ -229,28 +227,22 @@
async function uploadAnnotations(session, file, loader) { async function uploadAnnotations(session, file, loader) {
const sessionType = session instanceof Task ? 'task' : 'job'; const sessionType = session instanceof Task ? 'task' : 'job';
if (!(loader instanceof Loader)) { if (!(loader instanceof Loader)) {
throw new ArgumentError( throw new ArgumentError('A loader must be instance of Loader class');
'A loader must be instance of Loader class',
);
} }
await serverProxy.annotations.uploadAnnotations(sessionType, session.id, file, loader.name); await serverProxy.annotations.uploadAnnotations(sessionType, session.id, file, loader.name);
} }
async function dumpAnnotations(session, name, dumper) { async function dumpAnnotations(session, name, dumper) {
if (!(dumper instanceof Dumper)) { if (!(dumper instanceof Dumper)) {
throw new ArgumentError( throw new ArgumentError('A dumper must be instance of Dumper class');
'A dumper must be instance of Dumper class',
);
} }
let result = null; let result = null;
const sessionType = session instanceof Task ? 'task' : 'job'; const sessionType = session instanceof Task ? 'task' : 'job';
if (sessionType === 'job') { if (sessionType === 'job') {
result = await serverProxy.annotations result = await serverProxy.annotations.dumpAnnotations(session.task.id, name, dumper.name);
.dumpAnnotations(session.task.id, name, dumper.name);
} else { } else {
result = await serverProxy.annotations result = await serverProxy.annotations.dumpAnnotations(session.id, name, dumper.name);
.dumpAnnotations(session.id, name, dumper.name);
} }
return result; return result;
@ -284,19 +276,14 @@
async function exportDataset(session, format) { async function exportDataset(session, format) {
if (!(format instanceof String || typeof format === 'string')) { if (!(format instanceof String || typeof format === 'string')) {
throw new ArgumentError( throw new ArgumentError('Format must be a string');
'Format must be a string',
);
} }
if (!(session instanceof Task)) { if (!(session instanceof Task)) {
throw new ArgumentError( throw new ArgumentError('A dataset can only be created from a task');
'A dataset can only be created from a task',
);
} }
let result = null; let result = null;
result = await serverProxy.tasks result = await serverProxy.tasks.exportDataset(session.id, format);
.exportDataset(session.id, format);
return result; return result;
} }
@ -327,6 +314,19 @@
); );
} }
function freezeHistory(session, frozen) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).history.freeze(frozen);
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
function clearActions(session) { function clearActions(session) {
const sessionType = session instanceof Task ? 'task' : 'job'; const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType); const cache = getCache(sessionType);
@ -360,6 +360,7 @@
hasUnsavedChanges, hasUnsavedChanges,
mergeAnnotations, mergeAnnotations,
searchAnnotations, searchAnnotations,
searchEmptyFrame,
splitAnnotations, splitAnnotations,
groupAnnotations, groupAnnotations,
clearAnnotations, clearAnnotations,
@ -372,6 +373,7 @@
exportDataset, exportDataset,
undoActions, undoActions,
redoActions, redoActions,
freezeHistory,
clearActions, clearActions,
getActions, getActions,
closeSession, closeSession,

@ -1,52 +1,22 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019-2020 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
/* eslint prefer-arrow-callback: [ "error", { "allowNamedFunctions": true } ] */
/* global
require:false
*/
(() => { (() => {
const PluginRegistry = require('./plugins'); const PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy');
const lambdaManager = require('./lambda-manager'); const lambdaManager = require('./lambda-manager');
const { const {
isBoolean, isBoolean, isInteger, isEnum, isString, checkFilter,
isInteger,
isEnum,
isString,
checkFilter,
} = require('./common'); } = require('./common');
const { TaskStatus, TaskMode } = require('./enums'); const { TaskStatus, TaskMode } = require('./enums');
const User = require('./user'); const User = require('./user');
const { AnnotationFormats } = require('./annotation-formats.js'); const { AnnotationFormats } = require('./annotation-formats');
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
const { Task } = require('./session'); const { Task } = require('./session');
const { Project } = require('./project');
function attachUsers(task, users) {
if (task.assignee !== null) {
[task.assignee] = users.filter((user) => user.id === task.assignee);
}
for (const segment of task.segments) {
for (const job of segment.jobs) {
if (job.assignee !== null) {
[job.assignee] = users.filter((user) => user.id === job.assignee);
}
}
}
if (task.owner !== null) {
[task.owner] = users.filter((user) => user.id === task.owner);
}
return task;
}
function implementAPI(cvat) { function implementAPI(cvat) {
cvat.plugins.list.implementation = PluginRegistry.list; cvat.plugins.list.implementation = PluginRegistry.list;
@ -79,10 +49,24 @@
return result; return result;
}; };
cvat.server.register.implementation = async (username, firstName, lastName, cvat.server.register.implementation = async (
email, password1, password2, userConfirmations) => { username,
const user = await serverProxy.server.register(username, firstName, firstName,
lastName, email, password1, password2, userConfirmations); lastName,
email,
password1,
password2,
userConfirmations,
) => {
const user = await serverProxy.server.register(
username,
firstName,
lastName,
email,
password1,
password2,
userConfirmations,
);
return new User(user); return new User(user);
}; };
@ -99,6 +83,14 @@
await serverProxy.server.changePassword(oldPassword, newPassword1, newPassword2); await serverProxy.server.changePassword(oldPassword, newPassword1, newPassword2);
}; };
cvat.server.requestPasswordReset.implementation = async (email) => {
await serverProxy.server.requestPasswordReset(email);
};
cvat.server.resetPassword.implementation = async (newPassword1, newPassword2, uid, token) => {
await serverProxy.server.resetPassword(newPassword1, newPassword2, uid, token);
};
cvat.server.authorized.implementation = async () => { cvat.server.authorized.implementation = async () => {
const result = await serverProxy.server.authorized(); const result = await serverProxy.server.authorized();
return result; return result;
@ -109,17 +101,31 @@
return result; return result;
}; };
cvat.server.installedApps.implementation = async () => {
const result = await serverProxy.server.installedApps();
return result;
};
cvat.users.get.implementation = async (filter) => { cvat.users.get.implementation = async (filter) => {
checkFilter(filter, { checkFilter(filter, {
id: isInteger,
self: isBoolean, self: isBoolean,
search: isString,
limit: isInteger,
}); });
let users = null; let users = null;
if ('self' in filter && filter.self) { if ('self' in filter && filter.self) {
users = await serverProxy.users.getSelf(); users = await serverProxy.users.self();
users = [users]; users = [users];
} else { } else {
users = await serverProxy.users.getUsers(); const searchParams = {};
for (const key in filter) {
if (filter[key] && key !== 'self') {
searchParams[key] = filter[key];
}
}
users = await serverProxy.users.get(new URLSearchParams(searchParams).toString());
} }
users = users.map((user) => new User(user)); users = users.map((user) => new User(user));
@ -132,44 +138,37 @@
jobID: isInteger, jobID: isInteger,
}); });
if (('taskID' in filter) && ('jobID' in filter)) { if ('taskID' in filter && 'jobID' in filter) {
throw new ArgumentError( throw new ArgumentError('Only one of fields "taskID" and "jobID" allowed simultaneously');
'Only one of fields "taskID" and "jobID" allowed simultaneously',
);
} }
if (!Object.keys(filter).length) { if (!Object.keys(filter).length) {
throw new ArgumentError( throw new ArgumentError('Job filter must not be empty');
'Job filter must not be empty',
);
} }
let tasks = null; let tasks = [];
if ('taskID' in filter) { if ('taskID' in filter) {
tasks = await serverProxy.tasks.getTasks(`id=${filter.taskID}`); tasks = await serverProxy.tasks.getTasks(`id=${filter.taskID}`);
} else { } else {
const job = await serverProxy.jobs.getJob(filter.jobID); const job = await serverProxy.jobs.get(filter.jobID);
if (typeof (job.task_id) !== 'undefined') { if (typeof job.task_id !== 'undefined') {
tasks = await serverProxy.tasks.getTasks(`id=${job.task_id}`); 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 task was found by its id, then create task instance and get Job instance from it
if (tasks !== null && tasks.length) { if (tasks.length) {
const users = (await serverProxy.users.getUsers()) const task = new Task(tasks[0]);
.map((userData) => new User(userData)); return filter.jobID ? task.jobs.filter((job) => job.id === filter.jobID) : task.jobs;
const task = new Task(attachUsers(tasks[0], users));
return filter.jobID ? task.jobs
.filter((job) => job.id === filter.jobID) : task.jobs;
} }
return []; return tasks;
}; };
cvat.tasks.get.implementation = async (filter) => { cvat.tasks.get.implementation = async (filter) => {
checkFilter(filter, { checkFilter(filter, {
page: isInteger, page: isInteger,
projectId: isInteger,
name: isString, name: isString,
id: isInteger, id: isInteger,
owner: isString, owner: isString,
@ -181,40 +180,79 @@
if ('search' in filter && Object.keys(filter).length > 1) { if ('search' in filter && Object.keys(filter).length > 1) {
if (!('page' in filter && Object.keys(filter).length === 2)) { if (!('page' in filter && Object.keys(filter).length === 2)) {
throw new ArgumentError( throw new ArgumentError('Do not use the filter field "search" with others');
'Do not use the filter field "search" with others',
);
} }
} }
if ('id' in filter && Object.keys(filter).length > 1) { if ('id' in filter && Object.keys(filter).length > 1) {
if (!('page' in filter && Object.keys(filter).length === 2)) { if (!('page' in filter && Object.keys(filter).length === 2)) {
throw new ArgumentError( throw new ArgumentError('Do not use the filter field "id" with others');
'Do not use the filter field "id" with others',
);
} }
} }
if (
'projectId' in filter
&& (('page' in filter && Object.keys(filter).length > 2) || Object.keys(filter).length > 2)
) {
throw new ArgumentError('Do not use the filter field "projectId" with other');
}
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
for (const field of ['name', 'owner', 'assignee', 'search', 'status', 'mode', 'id', 'page']) { for (const field of ['name', 'owner', 'assignee', 'search', 'status', 'mode', 'id', 'page', 'projectId']) {
if (Object.prototype.hasOwnProperty.call(filter, field)) { if (Object.prototype.hasOwnProperty.call(filter, field)) {
searchParams.set(field, filter[field]); searchParams.set(field, filter[field]);
} }
} }
const users = (await serverProxy.users.getUsers())
.map((userData) => new User(userData));
const tasksData = await serverProxy.tasks.getTasks(searchParams.toString()); const tasksData = await serverProxy.tasks.getTasks(searchParams.toString());
const tasks = tasksData const tasks = tasksData.map((task) => new Task(task));
.map((task) => attachUsers(task, users))
.map((task) => new Task(task));
tasks.count = tasksData.count; tasks.count = tasksData.count;
return tasks; return tasks;
}; };
cvat.projects.get.implementation = async (filter) => {
checkFilter(filter, {
id: isInteger,
page: isInteger,
name: isString,
assignee: isString,
owner: isString,
search: isString,
status: isEnum.bind(TaskStatus),
});
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', 'assignee', 'owner', 'search', 'status', 'id', 'page']) {
if (Object.prototype.hasOwnProperty.call(filter, field)) {
searchParams.set(field, filter[field]);
}
}
const projectsData = await serverProxy.projects.get(searchParams.toString());
// prettier-ignore
const projects = projectsData.map((project) => new Project(project));
projects.count = projectsData.count;
return projects;
};
cvat.projects.searchNames.implementation = async (search, limit) => serverProxy.projects.searchNames(search, limit);
return cvat; return cvat;
} }

File diff suppressed because it is too large Load Diff

@ -0,0 +1,153 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
const User = require('./user');
const { ArgumentError } = require('./exceptions');
const { negativeIDGenerator } = require('./common');
/**
* Class representing a single comment
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Comment {
constructor(initialData) {
const data = {
id: undefined,
message: undefined,
created_date: undefined,
updated_date: undefined,
removed: false,
author: undefined,
};
for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
data[property] = initialData[property];
}
}
if (data.author && !(data.author instanceof User)) data.author = new User(data.author);
if (typeof id === 'undefined') {
data.id = negativeIDGenerator();
}
if (typeof data.created_date === 'undefined') {
data.created_date = new Date().toISOString();
}
Object.defineProperties(
this,
Object.freeze({
/**
* @name id
* @type {integer}
* @memberof module:API.cvat.classes.Comment
* @readonly
* @instance
*/
id: {
get: () => data.id,
},
/**
* @name message
* @type {string}
* @memberof module:API.cvat.classes.Comment
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
message: {
get: () => data.message,
set: (value) => {
if (!value.trim().length) {
throw new ArgumentError('Value must not be empty');
}
data.message = value;
},
},
/**
* @name createdDate
* @type {string}
* @memberof module:API.cvat.classes.Comment
* @readonly
* @instance
*/
createdDate: {
get: () => data.created_date,
},
/**
* @name updatedDate
* @type {string}
* @memberof module:API.cvat.classes.Comment
* @readonly
* @instance
*/
updatedDate: {
get: () => data.updated_date,
},
/**
* Instance of a user who has created the comment
* @name author
* @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Comment
* @readonly
* @instance
*/
author: {
get: () => data.author,
},
/**
* @name removed
* @type {boolean}
* @memberof module:API.cvat.classes.Comment
* @instance
*/
removed: {
get: () => data.removed,
set: (value) => {
if (typeof value !== 'boolean') {
throw new ArgumentError('Value must be a boolean value');
}
data.removed = value;
},
},
__internal: {
get: () => data,
},
}),
);
}
serialize() {
const data = {
message: this.message,
};
if (this.id > 0) {
data.id = this.id;
}
if (this.createdDate) {
data.created_date = this.createdDate;
}
if (this.updatedDate) {
data.updated_date = this.updatedDate;
}
if (this.author) {
data.author = this.author.serialize();
}
return data;
}
toJSON() {
const data = this.serialize();
const { author, ...updated } = data;
return {
...updated,
author_id: author ? author.id : undefined,
};
}
}
module.exports = Comment;

@ -1,21 +1,16 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
(() => { (() => {
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
function isBoolean(value) { function isBoolean(value) {
return typeof (value) === 'boolean'; return typeof value === 'boolean';
} }
function isInteger(value) { function isInteger(value) {
return typeof (value) === 'number' && Number.isInteger(value); return typeof value === 'number' && Number.isInteger(value);
} }
// Called with specific Enum context // Called with specific Enum context
@ -32,20 +27,16 @@
} }
function isString(value) { function isString(value) {
return typeof (value) === 'string'; return typeof value === 'string';
} }
function checkFilter(filter, fields) { function checkFilter(filter, fields) {
for (const prop in filter) { for (const prop in filter) {
if (Object.prototype.hasOwnProperty.call(filter, prop)) { if (Object.prototype.hasOwnProperty.call(filter, prop)) {
if (!(prop in fields)) { if (!(prop in fields)) {
throw new ArgumentError( throw new ArgumentError(`Unsupported filter property has been recieved: "${prop}"`);
`Unsupported filter property has been recieved: "${prop}"`,
);
} else if (!fields[prop](filter[prop])) { } else if (!fields[prop](filter[prop])) {
throw new ArgumentError( throw new ArgumentError(`Received filter property "${prop}" is not satisfied for checker`);
`Received filter property "${prop}" is not satisfied for checker`,
);
} }
} }
} }
@ -53,15 +44,13 @@
function checkObjectType(name, value, type, instance) { function checkObjectType(name, value, type, instance) {
if (type) { if (type) {
if (typeof (value) !== type) { if (typeof value !== type) {
// specific case for integers which aren't native type in JS // specific case for integers which aren't native type in JS
if (type === 'integer' && Number.isInteger(value)) { if (type === 'integer' && Number.isInteger(value)) {
return true; return true;
} }
throw new ArgumentError( throw new ArgumentError(`"${name}" is expected to be "${type}", but "${typeof value}" has been got.`);
`"${name}" is expected to be "${type}", but "${typeof (value)}" has been got.`,
);
} }
} else if (instance) { } else if (instance) {
if (!(value instanceof instance)) { if (!(value instanceof instance)) {
@ -72,15 +61,20 @@
); );
} }
throw new ArgumentError( throw new ArgumentError(`"${name}" is expected to be ${instance.name}, but "undefined" has been got.`);
`"${name}" is expected to be ${instance.name}, but "undefined" has been got.`,
);
} }
} }
return true; return true;
} }
function negativeIDGenerator() {
const value = negativeIDGenerator.start;
negativeIDGenerator.start -= 1;
return value;
}
negativeIDGenerator.start = -1;
module.exports = { module.exports = {
isBoolean, isBoolean,
isInteger, isInteger,
@ -88,5 +82,6 @@
isString, isString,
checkFilter, checkFilter,
checkObjectType, checkObjectType,
negativeIDGenerator,
}; };
})(); })();

@ -1,7 +1,6 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
module.exports = { module.exports = {
backendAPI: 'http://localhost:7000/api/v1', backendAPI: 'http://localhost:7000/api/v1',

@ -1,11 +1,6 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
const Axios = require('axios'); const Axios = require('axios');
@ -13,7 +8,6 @@ Axios.defaults.withCredentials = true;
Axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN'; Axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN';
Axios.defaults.xsrfCookieName = 'csrftoken'; Axios.defaults.xsrfCookieName = 'csrftoken';
onmessage = (e) => { onmessage = (e) => {
Axios.get(e.data.url, e.data.config) Axios.get(e.data.url, e.data.config)
.then((response) => { .then((response) => {
@ -26,7 +20,9 @@ onmessage = (e) => {
.catch((error) => { .catch((error) => {
postMessage({ postMessage({
id: e.data.id, id: e.data.id,
error, error: error,
status: error.response.status,
responseData: error.response.data,
isSuccess: false, isSuccess: false,
}); });
}); });

@ -1,33 +1,32 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019-2020 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
(() => { (() => {
/** /**
* Share files types * Share files types
* @enum {string} * @enum {string}
* @name ShareFileType * @name ShareFileType
* @memberof module:API.cvat.enums * @memberof module:API.cvat.enums
* @property {string} DIR 'DIR' * @property {string} DIR 'DIR'
* @property {string} REG 'REG' * @property {string} REG 'REG'
* @readonly * @readonly
*/ */
const ShareFileType = Object.freeze({ const ShareFileType = Object.freeze({
DIR: 'DIR', DIR: 'DIR',
REG: 'REG', REG: 'REG',
}); });
/** /**
* Task statuses * Task statuses
* @enum {string} * @enum {string}
* @name TaskStatus * @name TaskStatus
* @memberof module:API.cvat.enums * @memberof module:API.cvat.enums
* @property {string} ANNOTATION 'annotation' * @property {string} ANNOTATION 'annotation'
* @property {string} VALIDATION 'validation' * @property {string} VALIDATION 'validation'
* @property {string} COMPLETED 'completed' * @property {string} COMPLETED 'completed'
* @readonly * @readonly
*/ */
const TaskStatus = Object.freeze({ const TaskStatus = Object.freeze({
ANNOTATION: 'annotation', ANNOTATION: 'annotation',
VALIDATION: 'validation', VALIDATION: 'validation',
@ -35,18 +34,34 @@
}); });
/** /**
* List of RQ statuses * Review statuses
* @enum {string} * @enum {string}
* @name RQStatus * @name ReviewStatus
* @memberof module:API.cvat.enums * @memberof module:API.cvat.enums
* @property {string} QUEUED 'queued' * @property {string} ACCEPTED 'accepted'
* @property {string} STARTED 'started' * @property {string} REJECTED 'rejected'
* @property {string} FINISHED 'finished' * @property {string} REVIEW_FURTHER 'review_further'
* @property {string} FAILED 'failed' * @readonly
* @property {string} UNKNOWN 'unknown' */
* @readonly const ReviewStatus = Object.freeze({
*/ ACCEPTED: 'accepted',
const RQStatus = Object.freeze({ REJECTED: 'rejected',
REVIEW_FURTHER: 'review_further',
});
/**
* List of RQ statuses
* @enum {string}
* @name RQStatus
* @memberof module:API.cvat.enums
* @property {string} QUEUED 'queued'
* @property {string} STARTED 'started'
* @property {string} FINISHED 'finished'
* @property {string} FAILED 'failed'
* @property {string} UNKNOWN 'unknown'
* @readonly
*/
const RQStatus = Object.freeze({
QUEUED: 'queued', QUEUED: 'queued',
STARTED: 'started', STARTED: 'started',
FINISHED: 'finished', FINISHED: 'finished',
@ -55,31 +70,31 @@
}); });
/** /**
* Task modes * Task modes
* @enum {string} * @enum {string}
* @name TaskMode * @name TaskMode
* @memberof module:API.cvat.enums * @memberof module:API.cvat.enums
* @property {string} ANNOTATION 'annotation' * @property {string} ANNOTATION 'annotation'
* @property {string} INTERPOLATION 'interpolation' * @property {string} INTERPOLATION 'interpolation'
* @readonly * @readonly
*/ */
const TaskMode = Object.freeze({ const TaskMode = Object.freeze({
ANNOTATION: 'annotation', ANNOTATION: 'annotation',
INTERPOLATION: 'interpolation', INTERPOLATION: 'interpolation',
}); });
/** /**
* Attribute types * Attribute types
* @enum {string} * @enum {string}
* @name AttributeType * @name AttributeType
* @memberof module:API.cvat.enums * @memberof module:API.cvat.enums
* @property {string} CHECKBOX 'checkbox' * @property {string} CHECKBOX 'checkbox'
* @property {string} SELECT 'select' * @property {string} SELECT 'select'
* @property {string} RADIO 'radio' * @property {string} RADIO 'radio'
* @property {string} NUMBER 'number' * @property {string} NUMBER 'number'
* @property {string} TEXT 'text' * @property {string} TEXT 'text'
* @readonly * @readonly
*/ */
const AttributeType = Object.freeze({ const AttributeType = Object.freeze({
CHECKBOX: 'checkbox', CHECKBOX: 'checkbox',
RADIO: 'radio', RADIO: 'radio',
@ -89,15 +104,15 @@
}); });
/** /**
* Object types * Object types
* @enum {string} * @enum {string}
* @name ObjectType * @name ObjectType
* @memberof module:API.cvat.enums * @memberof module:API.cvat.enums
* @property {string} TAG 'tag' * @property {string} TAG 'tag'
* @property {string} SHAPE 'shape' * @property {string} SHAPE 'shape'
* @property {string} TRACK 'track' * @property {string} TRACK 'track'
* @readonly * @readonly
*/ */
const ObjectType = Object.freeze({ const ObjectType = Object.freeze({
TAG: 'tag', TAG: 'tag',
SHAPE: 'shape', SHAPE: 'shape',
@ -105,17 +120,17 @@
}); });
/** /**
* Object shapes * Object shapes
* @enum {string} * @enum {string}
* @name ObjectShape * @name ObjectShape
* @memberof module:API.cvat.enums * @memberof module:API.cvat.enums
* @property {string} RECTANGLE 'rectangle' * @property {string} RECTANGLE 'rectangle'
* @property {string} POLYGON 'polygon' * @property {string} POLYGON 'polygon'
* @property {string} POLYLINE 'polyline' * @property {string} POLYLINE 'polyline'
* @property {string} POINTS 'points' * @property {string} POINTS 'points'
* @property {string} CUBOID 'cuboid' * @property {string} CUBOID 'cuboid'
* @readonly * @readonly
*/ */
const ObjectShape = Object.freeze({ const ObjectShape = Object.freeze({
RECTANGLE: 'rectangle', RECTANGLE: 'rectangle',
POLYGON: 'polygon', POLYGON: 'polygon',
@ -125,17 +140,17 @@
}); });
/** /**
* Annotation type * Annotation type
* @enum {string} * @enum {string}
* @name Source * @name Source
* @memberof module:API.cvat.enums * @memberof module:API.cvat.enums
* @property {string} MANUAL 'manual' * @property {string} MANUAL 'manual'
* @property {string} AUTO 'auto' * @property {string} AUTO 'auto'
* @readonly * @readonly
*/ */
const Source = Object.freeze({ const Source = Object.freeze({
MANUAL:'manual', MANUAL: 'manual',
AUTO:'auto', AUTO: 'auto',
}); });
/** /**
@ -211,27 +226,27 @@
}); });
/** /**
* Types of actions with annotations * Types of actions with annotations
* @enum {string} * @enum {string}
* @name HistoryActions * @name HistoryActions
* @memberof module:API.cvat.enums * @memberof module:API.cvat.enums
* @property {string} CHANGED_LABEL Changed label * @property {string} CHANGED_LABEL Changed label
* @property {string} CHANGED_ATTRIBUTES Changed attributes * @property {string} CHANGED_ATTRIBUTES Changed attributes
* @property {string} CHANGED_POINTS Changed points * @property {string} CHANGED_POINTS Changed points
* @property {string} CHANGED_OUTSIDE Changed outside * @property {string} CHANGED_OUTSIDE Changed outside
* @property {string} CHANGED_OCCLUDED Changed occluded * @property {string} CHANGED_OCCLUDED Changed occluded
* @property {string} CHANGED_ZORDER Changed z-order * @property {string} CHANGED_ZORDER Changed z-order
* @property {string} CHANGED_LOCK Changed lock * @property {string} CHANGED_LOCK Changed lock
* @property {string} CHANGED_COLOR Changed color * @property {string} CHANGED_COLOR Changed color
* @property {string} CHANGED_HIDDEN Changed hidden * @property {string} CHANGED_HIDDEN Changed hidden
* @property {string} CHANGED_SOURCE Changed source * @property {string} CHANGED_SOURCE Changed source
* @property {string} MERGED_OBJECTS Merged objects * @property {string} MERGED_OBJECTS Merged objects
* @property {string} SPLITTED_TRACK Splitted track * @property {string} SPLITTED_TRACK Splitted track
* @property {string} GROUPED_OBJECTS Grouped objects * @property {string} GROUPED_OBJECTS Grouped objects
* @property {string} CREATED_OBJECTS Created objects * @property {string} CREATED_OBJECTS Created objects
* @property {string} REMOVED_OBJECT Removed object * @property {string} REMOVED_OBJECT Removed object
* @readonly * @readonly
*/ */
const HistoryActions = Object.freeze({ const HistoryActions = Object.freeze({
CHANGED_LABEL: 'Changed label', CHANGED_LABEL: 'Changed label',
CHANGED_ATTRIBUTES: 'Changed attributes', CHANGED_ATTRIBUTES: 'Changed attributes',
@ -265,23 +280,49 @@
}; };
/** /**
* Array of hex colors * Array of hex colors
* @name colors * @name colors
* @memberof module:API.cvat.enums * @memberof module:API.cvat.enums
* @type {string[]} * @type {string[]}
* @readonly * @readonly
*/ */
const colors = [ const colors = [
'#33ddff', '#fa3253', '#34d1b7', '#ff007c', '#ff6037', '#ddff33', '#33ddff',
'#24b353', '#b83df5', '#66ff66', '#32b7fa', '#ffcc33', '#83e070', '#fa3253',
'#fafa37', '#5986b3', '#8c78f0', '#ff6a4d', '#f078f0', '#2a7dd1', '#34d1b7',
'#b25050', '#cc3366', '#cc9933', '#aaf0d1', '#ff00cc', '#3df53d', '#ff007c',
'#fa32b7', '#fa7dbb', '#ff355e', '#f59331', '#3d3df5', '#733380', '#ff6037',
'#ddff33',
'#24b353',
'#b83df5',
'#66ff66',
'#32b7fa',
'#ffcc33',
'#83e070',
'#fafa37',
'#5986b3',
'#8c78f0',
'#ff6a4d',
'#f078f0',
'#2a7dd1',
'#b25050',
'#cc3366',
'#cc9933',
'#aaf0d1',
'#ff00cc',
'#3df53d',
'#fa32b7',
'#fa7dbb',
'#ff355e',
'#f59331',
'#3d3df5',
'#733380',
]; ];
module.exports = { module.exports = {
ShareFileType, ShareFileType,
TaskStatus, TaskStatus,
ReviewStatus,
TaskMode, TaskMode,
AttributeType, AttributeType,
ObjectType, ObjectType,

@ -1,11 +1,6 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
(() => { (() => {
const Platform = require('platform'); const Platform = require('platform');
@ -13,15 +8,15 @@
const config = require('./config'); const config = require('./config');
/** /**
* Base exception class * Base exception class
* @memberof module:API.cvat.exceptions * @memberof module:API.cvat.exceptions
* @extends Error * @extends Error
* @ignore * @ignore
*/ */
class Exception extends Error { class Exception extends Error {
/** /**
* @param {string} message - Exception message * @param {string} message - Exception message
*/ */
constructor(message) { constructor(message) {
super(message); super(message);
@ -32,126 +27,125 @@
const filename = `${info.fileName}`; const filename = `${info.fileName}`;
const line = info.lineNumber; const line = info.lineNumber;
const column = info.columnNumber; const column = info.columnNumber;
const { const { jobID, taskID, clientID } = config;
jobID,
taskID,
clientID,
} = config;
const projID = undefined; // wasn't implemented const projID = undefined; // wasn't implemented
Object.defineProperties(this, Object.freeze({ Object.defineProperties(
system: { this,
/** Object.freeze({
* @name system system: {
* @type {string} /**
* @memberof module:API.cvat.exceptions.Exception * @name system
* @readonly * @type {string}
* @instance * @memberof module:API.cvat.exceptions.Exception
*/ * @readonly
get: () => system, * @instance
}, */
client: { get: () => system,
/** },
* @name client client: {
* @type {string} /**
* @memberof module:API.cvat.exceptions.Exception * @name client
* @readonly * @type {string}
* @instance * @memberof module:API.cvat.exceptions.Exception
*/ * @readonly
get: () => client, * @instance
}, */
time: { get: () => client,
/** },
* @name time time: {
* @type {string} /**
* @memberof module:API.cvat.exceptions.Exception * @name time
* @readonly * @type {string}
* @instance * @memberof module:API.cvat.exceptions.Exception
*/ * @readonly
get: () => time, * @instance
}, */
jobID: { get: () => time,
/** },
* @name jobID jobID: {
* @type {integer} /**
* @memberof module:API.cvat.exceptions.Exception * @name jobID
* @readonly * @type {integer}
* @instance * @memberof module:API.cvat.exceptions.Exception
*/ * @readonly
get: () => jobID, * @instance
}, */
taskID: { get: () => jobID,
/** },
* @name taskID taskID: {
* @type {integer} /**
* @memberof module:API.cvat.exceptions.Exception * @name taskID
* @readonly * @type {integer}
* @instance * @memberof module:API.cvat.exceptions.Exception
*/ * @readonly
get: () => taskID, * @instance
}, */
projID: { get: () => taskID,
/** },
* @name projID projID: {
* @type {integer} /**
* @memberof module:API.cvat.exceptions.Exception * @name projID
* @readonly * @type {integer}
* @instance * @memberof module:API.cvat.exceptions.Exception
*/ * @readonly
get: () => projID, * @instance
}, */
clientID: { get: () => projID,
/** },
* @name clientID clientID: {
* @type {integer} /**
* @memberof module:API.cvat.exceptions.Exception * @name clientID
* @readonly * @type {integer}
* @instance * @memberof module:API.cvat.exceptions.Exception
*/ * @readonly
get: () => clientID, * @instance
}, */
filename: { get: () => clientID,
/** },
* @name filename filename: {
* @type {string} /**
* @memberof module:API.cvat.exceptions.Exception * @name filename
* @readonly * @type {string}
* @instance * @memberof module:API.cvat.exceptions.Exception
*/ * @readonly
get: () => filename, * @instance
}, */
line: { get: () => filename,
/** },
* @name line line: {
* @type {integer} /**
* @memberof module:API.cvat.exceptions.Exception * @name line
* @readonly * @type {integer}
* @instance * @memberof module:API.cvat.exceptions.Exception
*/ * @readonly
get: () => line, * @instance
}, */
column: { get: () => line,
/** },
* @name column column: {
* @type {integer} /**
* @memberof module:API.cvat.exceptions.Exception * @name column
* @readonly * @type {integer}
* @instance * @memberof module:API.cvat.exceptions.Exception
*/ * @readonly
get: () => column, * @instance
}, */
})); get: () => column,
},
}),
);
} }
/** /**
* Save an exception on a server * Save an exception on a server
* @name save * @name save
* @method * @method
* @memberof Exception * @memberof Exception
* @instance * @instance
* @async * @async
*/ */
async save() { async save() {
const exceptionObject = { const exceptionObject = {
system: this.system, system: this.system,
@ -178,86 +172,89 @@
} }
/** /**
* Exceptions are referred with arguments data * Exceptions are referred with arguments data
* @memberof module:API.cvat.exceptions * @memberof module:API.cvat.exceptions
* @extends module:API.cvat.exceptions.Exception * @extends module:API.cvat.exceptions.Exception
*/ */
class ArgumentError extends Exception { class ArgumentError extends Exception {
/** /**
* @param {string} message - Exception message * @param {string} message - Exception message
*/ */
constructor(message) { constructor(message) {
super(message); super(message);
} }
} }
/** /**
* Unexpected problems with data which are not connected with a user input * Unexpected problems with data which are not connected with a user input
* @memberof module:API.cvat.exceptions * @memberof module:API.cvat.exceptions
* @extends module:API.cvat.exceptions.Exception * @extends module:API.cvat.exceptions.Exception
*/ */
class DataError extends Exception { class DataError extends Exception {
/** /**
* @param {string} message - Exception message * @param {string} message - Exception message
*/ */
constructor(message) { constructor(message) {
super(message); super(message);
} }
} }
/** /**
* Unexpected situations in code * Unexpected situations in code
* @memberof module:API.cvat.exceptions * @memberof module:API.cvat.exceptions
* @extends module:API.cvat.exceptions.Exception * @extends module:API.cvat.exceptions.Exception
*/ */
class ScriptingError extends Exception { class ScriptingError extends Exception {
/** /**
* @param {string} message - Exception message * @param {string} message - Exception message
*/ */
constructor(message) { constructor(message) {
super(message); super(message);
} }
} }
/** /**
* Plugin-referred exceptions * Plugin-referred exceptions
* @memberof module:API.cvat.exceptions * @memberof module:API.cvat.exceptions
* @extends module:API.cvat.exceptions.Exception * @extends module:API.cvat.exceptions.Exception
*/ */
class PluginError extends Exception { class PluginError extends Exception {
/** /**
* @param {string} message - Exception message * @param {string} message - Exception message
*/ */
constructor(message) { constructor(message) {
super(message); super(message);
} }
} }
/** /**
* Exceptions in interaction with a server * Exceptions in interaction with a server
* @memberof module:API.cvat.exceptions * @memberof module:API.cvat.exceptions
* @extends module:API.cvat.exceptions.Exception * @extends module:API.cvat.exceptions.Exception
*/ */
class ServerError extends Exception { class ServerError extends Exception {
/** /**
* @param {string} message - Exception message * @param {string} message - Exception message
* @param {(string|integer)} code - Response code * @param {(string|integer)} code - Response code
*/ */
constructor(message, code) { constructor(message, code) {
super(message); super(message);
Object.defineProperties(this, Object.freeze({ Object.defineProperties(
/** this,
* @name code Object.freeze({
* @type {(string|integer)} /**
* @memberof module:API.cvat.exceptions.ServerError * @name code
* @readonly * @type {(string|integer)}
* @instance * @memberof module:API.cvat.exceptions.ServerError
*/ * @readonly
code: { * @instance
get: () => code, */
}, code: {
})); get: () => code,
},
}),
);
} }
} }

@ -1,12 +1,6 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
/* global
require:false
global:false
*/
(() => { (() => {
const cvatData = require('cvat-data'); const cvatData = require('cvat-data');
@ -19,100 +13,95 @@
const frameDataCache = {}; const frameDataCache = {};
/** /**
* Class provides meta information about specific frame and frame itself * Class provides meta information about specific frame and frame itself
* @memberof module:API.cvat.classes * @memberof module:API.cvat.classes
* @hideconstructor * @hideconstructor
*/ */
class FrameData { class FrameData {
constructor({ constructor({
width, width, height, name, taskID, frameNumber, startFrame, stopFrame, decodeForward,
height,
name,
taskID,
frameNumber,
startFrame,
stopFrame,
decodeForward,
}) { }) {
Object.defineProperties(this, Object.freeze({ Object.defineProperties(
/** this,
* @name filename Object.freeze({
* @type {string} /**
* @memberof module:API.cvat.classes.FrameData * @name filename
* @readonly * @type {string}
* @instance * @memberof module:API.cvat.classes.FrameData
*/ * @readonly
filename: { * @instance
value: name, */
writable: false, filename: {
}, value: name,
/** writable: false,
* @name width },
* @type {integer} /**
* @memberof module:API.cvat.classes.FrameData * @name width
* @readonly * @type {integer}
* @instance * @memberof module:API.cvat.classes.FrameData
*/ * @readonly
width: { * @instance
value: width, */
writable: false, width: {
}, value: width,
/** writable: false,
* @name height },
* @type {integer} /**
* @memberof module:API.cvat.classes.FrameData * @name height
* @readonly * @type {integer}
* @instance * @memberof module:API.cvat.classes.FrameData
*/ * @readonly
height: { * @instance
value: height, */
writable: false, height: {
}, value: height,
tid: { writable: false,
value: taskID, },
writable: false, tid: {
}, value: taskID,
/** writable: false,
* @name number },
* @type {integer} /**
* @memberof module:API.cvat.classes.FrameData * @name number
* @readonly * @type {integer}
* @instance * @memberof module:API.cvat.classes.FrameData
*/ * @readonly
number: { * @instance
value: frameNumber, */
writable: false, number: {
}, value: frameNumber,
startFrame: { writable: false,
value: startFrame, },
writable: false, startFrame: {
}, value: startFrame,
stopFrame: { writable: false,
value: stopFrame, },
writable: false, stopFrame: {
}, value: stopFrame,
decodeForward: { writable: false,
value: decodeForward, },
writable: false, decodeForward: {
}, value: decodeForward,
})); writable: false,
},
}),
);
} }
/** /**
* Method returns URL encoded image which can be placed in the img tag * Method returns URL encoded image which can be placed in the img tag
* @method data * @method data
* @returns {string} * @returns {string}
* @memberof module:API.cvat.classes.FrameData * @memberof module:API.cvat.classes.FrameData
* @instance * @instance
* @async * @async
* @param {function} [onServerRequest = () => {}] * @param {function} [onServerRequest = () => {}]
* callback which will be called if data absences local * callback which will be called if data absences local
* @throws {module:API.cvat.exception.ServerError} * @throws {module:API.cvat.exception.ServerError}
* @throws {module:API.cvat.exception.PluginError} * @throws {module:API.cvat.exception.PluginError}
*/ */
async data(onServerRequest = () => {}) { async data(onServerRequest = () => {}) {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper.call(this, FrameData.prototype.data, onServerRequest);
.apiWrapper.call(this, FrameData.prototype.data, onServerRequest);
return result; return result;
} }
} }
@ -136,15 +125,14 @@
const { provider } = frameDataCache[this.tid]; const { provider } = frameDataCache[this.tid];
const { chunkSize } = frameDataCache[this.tid]; const { chunkSize } = frameDataCache[this.tid];
const start = parseInt(this.number / chunkSize, 10) * chunkSize; const start = parseInt(this.number / chunkSize, 10) * chunkSize;
const stop = Math.min( const stop = Math.min(this.stopFrame, (parseInt(this.number / chunkSize, 10) + 1) * chunkSize - 1);
this.stopFrame,
(parseInt(this.number / chunkSize, 10) + 1) * chunkSize - 1,
);
const chunkNumber = Math.floor(this.number / chunkSize); const chunkNumber = Math.floor(this.number / chunkSize);
const onDecodeAll = async (frameNumber) => { const onDecodeAll = async (frameNumber) => {
if (frameDataCache[this.tid].activeChunkRequest if (
&& chunkNumber === frameDataCache[this.tid].activeChunkRequest.chunkNumber) { frameDataCache[this.tid].activeChunkRequest
&& chunkNumber === frameDataCache[this.tid].activeChunkRequest.chunkNumber
) {
const callbackArray = frameDataCache[this.tid].activeChunkRequest.callbacks; const callbackArray = frameDataCache[this.tid].activeChunkRequest.callbacks;
for (let i = callbackArray.length - 1; i >= 0; --i) { for (let i = callbackArray.length - 1; i >= 0; --i) {
if (callbackArray[i].frameNumber === frameNumber) { if (callbackArray[i].frameNumber === frameNumber) {
@ -160,8 +148,10 @@
}; };
const rejectRequestAll = () => { const rejectRequestAll = () => {
if (frameDataCache[this.tid].activeChunkRequest if (
&& chunkNumber === frameDataCache[this.tid].activeChunkRequest.chunkNumber) { frameDataCache[this.tid].activeChunkRequest
&& chunkNumber === frameDataCache[this.tid].activeChunkRequest.chunkNumber
) {
for (const r of frameDataCache[this.tid].activeChunkRequest.callbacks) { for (const r of frameDataCache[this.tid].activeChunkRequest.callbacks) {
r.reject(r.frameNumber); r.reject(r.frameNumber);
} }
@ -172,147 +162,160 @@
const makeActiveRequest = () => { const makeActiveRequest = () => {
const taskDataCache = frameDataCache[this.tid]; const taskDataCache = frameDataCache[this.tid];
const activeChunk = taskDataCache.activeChunkRequest; const activeChunk = taskDataCache.activeChunkRequest;
activeChunk.request = serverProxy.frames.getData(this.tid, activeChunk.request = serverProxy.frames
activeChunk.chunkNumber).then((chunk) => { .getData(this.tid, activeChunk.chunkNumber)
frameDataCache[this.tid].activeChunkRequest.completed = true; .then((chunk) => {
if (!taskDataCache.nextChunkRequest) { frameDataCache[this.tid].activeChunkRequest.completed = true;
provider.requestDecodeBlock(chunk, if (!taskDataCache.nextChunkRequest) {
taskDataCache.activeChunkRequest.start, provider.requestDecodeBlock(
taskDataCache.activeChunkRequest.stop, chunk,
taskDataCache.activeChunkRequest.onDecodeAll, taskDataCache.activeChunkRequest.start,
taskDataCache.activeChunkRequest.rejectRequestAll); taskDataCache.activeChunkRequest.stop,
} taskDataCache.activeChunkRequest.onDecodeAll,
}).catch((exception) => { taskDataCache.activeChunkRequest.rejectRequestAll,
if (exception instanceof Exception) { );
reject(exception); }
} else { })
reject(new Exception(exception.message)); .catch((exception) => {
} if (exception instanceof Exception) {
}).finally(() => { reject(exception);
if (taskDataCache.nextChunkRequest) { } else {
if (taskDataCache.activeChunkRequest) { reject(new Exception(exception.message));
for (const r of taskDataCache.activeChunkRequest.callbacks) { }
r.reject(r.frameNumber); })
.finally(() => {
if (taskDataCache.nextChunkRequest) {
if (taskDataCache.activeChunkRequest) {
for (const r of taskDataCache.activeChunkRequest.callbacks) {
r.reject(r.frameNumber);
}
} }
taskDataCache.activeChunkRequest = taskDataCache.nextChunkRequest;
taskDataCache.nextChunkRequest = null;
makeActiveRequest();
} }
taskDataCache.activeChunkRequest = taskDataCache.nextChunkRequest; });
taskDataCache.nextChunkRequest = null;
makeActiveRequest();
}
});
}; };
if (isNode) { if (isNode) {
resolve('Dummy data'); resolve('Dummy data');
} else if (isBrowser) { } else if (isBrowser) {
provider.frame(this.number).then((frame) => { provider
if (frame === null) { .frame(this.number)
onServerRequest(); .then((frame) => {
const activeRequest = frameDataCache[this.tid].activeChunkRequest; if (frame === null) {
if (!provider.isChunkCached(start, stop)) { onServerRequest();
if (!activeRequest const activeRequest = frameDataCache[this.tid].activeChunkRequest;
|| (activeRequest if (!provider.isChunkCached(start, stop)) {
&& activeRequest.completed if (
&& activeRequest.chunkNumber !== chunkNumber)) { !activeRequest
if (activeRequest && activeRequest.rejectRequestAll) { || (activeRequest
activeRequest.rejectRequestAll(); && activeRequest.completed
} && activeRequest.chunkNumber !== chunkNumber)
frameDataCache[this.tid].activeChunkRequest = { ) {
request: null, if (activeRequest && activeRequest.rejectRequestAll) {
chunkNumber, activeRequest.rejectRequestAll();
start, }
stop, frameDataCache[this.tid].activeChunkRequest = {
onDecodeAll, request: null,
rejectRequestAll, chunkNumber,
completed: false, start,
callbacks: [{ stop,
onDecodeAll,
rejectRequestAll,
completed: false,
callbacks: [
{
resolve: resolveWrapper,
reject,
frameNumber: this.number,
},
],
};
makeActiveRequest();
} else if (activeRequest.chunkNumber === chunkNumber) {
if (!activeRequest.onDecodeAll && !activeRequest.rejectRequestAll) {
activeRequest.onDecodeAll = onDecodeAll;
activeRequest.rejectRequestAll = rejectRequestAll;
}
activeRequest.callbacks.push({
resolve: resolveWrapper, resolve: resolveWrapper,
reject, reject,
frameNumber: this.number, frameNumber: this.number,
}], });
}; } else {
makeActiveRequest(); if (frameDataCache[this.tid].nextChunkRequest) {
} else if (activeRequest.chunkNumber === chunkNumber) { const { callbacks } = frameDataCache[this.tid].nextChunkRequest;
if (!activeRequest.onDecodeAll for (const r of callbacks) {
&& !activeRequest.rejectRequestAll) { r.reject(r.frameNumber);
activeRequest.onDecodeAll = onDecodeAll; }
activeRequest.rejectRequestAll = rejectRequestAll; }
frameDataCache[this.tid].nextChunkRequest = {
request: null,
chunkNumber,
start,
stop,
onDecodeAll,
rejectRequestAll,
completed: false,
callbacks: [
{
resolve: resolveWrapper,
reject,
frameNumber: this.number,
},
],
};
} }
} else {
activeRequest.callbacks.push({ activeRequest.callbacks.push({
resolve: resolveWrapper, resolve: resolveWrapper,
reject, reject,
frameNumber: this.number, frameNumber: this.number,
}); });
} else { provider.requestDecodeBlock(null, start, stop, onDecodeAll, rejectRequestAll);
if (frameDataCache[this.tid].nextChunkRequest) {
const { callbacks } = frameDataCache[this.tid].nextChunkRequest;
for (const r of callbacks) {
r.reject(r.frameNumber);
}
}
frameDataCache[this.tid].nextChunkRequest = {
request: null,
chunkNumber,
start,
stop,
onDecodeAll,
rejectRequestAll,
completed: false,
callbacks: [{
resolve: resolveWrapper,
reject,
frameNumber: this.number,
}],
};
} }
} else { } else {
activeRequest.callbacks.push({ if (
resolve: resolveWrapper, this.number % chunkSize > chunkSize / 4
reject, && provider.decodedBlocksCacheSize > 1
frameNumber: this.number, && this.decodeForward
}); && !provider.isNextChunkExists(this.number)
provider.requestDecodeBlock(null, start, stop, ) {
onDecodeAll, rejectRequestAll); const nextChunkNumber = Math.floor(this.number / chunkSize) + 1;
} if (nextChunkNumber * chunkSize < this.stopFrame) {
} else { provider.setReadyToLoading(nextChunkNumber);
if (this.number % chunkSize > chunkSize / 4 const nextStart = nextChunkNumber * chunkSize;
&& provider.decodedBlocksCacheSize > 1 const nextStop = (nextChunkNumber + 1) * chunkSize - 1;
&& this.decodeForward if (!provider.isChunkCached(nextStart, nextStop)) {
&& !provider.isNextChunkExists(this.number)) { if (!frameDataCache[this.tid].activeChunkRequest) {
const nextChunkNumber = Math.floor(this.number / chunkSize) + 1; frameDataCache[this.tid].activeChunkRequest = {
if (nextChunkNumber * chunkSize < this.stopFrame) { request: null,
provider.setReadyToLoading(nextChunkNumber); chunkNumber: nextChunkNumber,
const nextStart = nextChunkNumber * chunkSize; start: nextStart,
const nextStop = (nextChunkNumber + 1) * chunkSize - 1; stop: nextStop,
if (!provider.isChunkCached(nextStart, nextStop)) { onDecodeAll: null,
if (!frameDataCache[this.tid].activeChunkRequest) { rejectRequestAll: null,
frameDataCache[this.tid].activeChunkRequest = { completed: false,
request: null, callbacks: [],
chunkNumber: nextChunkNumber, };
start: nextStart, makeActiveRequest();
stop: nextStop, }
onDecodeAll: null, } else {
rejectRequestAll: null, provider.requestDecodeBlock(null, nextStart, nextStop, null, null);
completed: false,
callbacks: [],
};
makeActiveRequest();
} }
} else {
provider.requestDecodeBlock(null, nextStart, nextStop,
null, null);
} }
} }
resolveWrapper(frame);
} }
resolveWrapper(frame); })
} .catch((exception) => {
}).catch((exception) => { if (exception instanceof Exception) {
if (exception instanceof Exception) { reject(exception);
reject(exception); } else {
} else { reject(new Exception(exception.message));
reject(new Exception(exception.message)); }
} });
});
} }
}); });
}; };
@ -324,16 +327,12 @@
[size] = meta.frames; [size] = meta.frames;
} else if (mode === 'annotation') { } else if (mode === 'annotation') {
if (frame >= meta.size) { if (frame >= meta.size) {
throw new ArgumentError( throw new ArgumentError(`Meta information about frame ${frame} can't be received from the server`);
`Meta information about frame ${frame} can't be received from the server`,
);
} else { } else {
size = meta.frames[frame]; size = meta.frames[frame];
} }
} else { } else {
throw new DataError( throw new DataError(`Invalid mode is specified ${mode}`);
`Invalid mode is specified ${mode}`,
);
} }
return size; return size;
} }
@ -377,23 +376,28 @@
decodeForward: false, decodeForward: false,
}); });
frameData.data().then(() => { frameData
if (!(chunkIdx in this._requestedChunks) .data()
|| !this._requestedChunks[chunkIdx].requestedFrames.has(requestedFrame)) { .then(() => {
reject(chunkIdx); if (
} else { !(chunkIdx in this._requestedChunks)
this._requestedChunks[chunkIdx].requestedFrames.delete(requestedFrame); || !this._requestedChunks[chunkIdx].requestedFrames.has(requestedFrame)
this._requestedChunks[chunkIdx].buffer[requestedFrame] = frameData; ) {
if (this._requestedChunks[chunkIdx].requestedFrames.size === 0) { reject(chunkIdx);
const bufferedframes = Object.keys( } else {
this._requestedChunks[chunkIdx].buffer, this._requestedChunks[chunkIdx].requestedFrames.delete(requestedFrame);
).map((f) => +f); this._requestedChunks[chunkIdx].buffer[requestedFrame] = frameData;
this._requestedChunks[chunkIdx].resolve(new Set(bufferedframes)); if (this._requestedChunks[chunkIdx].requestedFrames.size === 0) {
const bufferedframes = Object.keys(this._requestedChunks[chunkIdx].buffer).map(
(f) => +f,
);
this._requestedChunks[chunkIdx].resolve(new Set(bufferedframes));
}
} }
} })
}).catch(() => { .catch(() => {
reject(chunkIdx); reject(chunkIdx);
}); });
} }
}); });
} }
@ -455,7 +459,7 @@
await this.fillBuffer(start, step, count); await this.fillBuffer(start, step, count);
this._activeFillBufferRequest = false; this._activeFillBufferRequest = false;
} catch (error) { } catch (error) {
if (typeof (error) === 'number' && error in this._requestedChunks) { if (typeof error === 'number' && error in this._requestedChunks) {
this._activeFillBufferRequest = false; this._activeFillBufferRequest = false;
} }
throw error; throw error;
@ -465,8 +469,7 @@
async require(frameNumber, taskID, fillBuffer, frameStep) { async require(frameNumber, taskID, fillBuffer, frameStep) {
for (const frame in this._buffer) { for (const frame in this._buffer) {
if (frame < frameNumber if (frame < frameNumber || frame >= frameNumber + this._size * frameStep) {
|| frame >= frameNumber + this._size * frameStep) {
delete this._buffer[frame]; delete this._buffer[frame];
} }
} }
@ -486,9 +489,12 @@
frame = this._buffer[frameNumber]; frame = this._buffer[frameNumber];
delete this._buffer[frameNumber]; delete this._buffer[frameNumber];
const cachedFrames = this.cachedFrames(); const cachedFrames = this.cachedFrames();
if (fillBuffer && !this._activeFillBufferRequest if (
fillBuffer
&& !this._activeFillBufferRequest
&& this._size > this._chunkSize && this._size > this._chunkSize
&& cachedFrames.length < (this._size * 3) / 4) { && cachedFrames.length < (this._size * 3) / 4
) {
const maxFrame = cachedFrames ? Math.max(...cachedFrames) : frameNumber; const maxFrame = cachedFrames ? Math.max(...cachedFrames) : frameNumber;
if (maxFrame < this._stopFrame) { if (maxFrame < this._stopFrame) {
this.makeFillRequest(maxFrame + 1, frameStep).catch((e) => { this.makeFillRequest(maxFrame + 1, frameStep).catch((e) => {
@ -512,8 +518,10 @@
clear() { clear() {
for (const chunkIdx in this._requestedChunks) { for (const chunkIdx in this._requestedChunks) {
if (Object.prototype.hasOwnProperty.call(this._requestedChunks, chunkIdx) if (
&& this._requestedChunks[chunkIdx].reject) { Object.prototype.hasOwnProperty.call(this._requestedChunks, chunkIdx)
&& this._requestedChunks[chunkIdx].reject
) {
this._requestedChunks[chunkIdx].reject('not needed'); this._requestedChunks[chunkIdx].reject('not needed');
} }
} }
@ -530,38 +538,39 @@
async function getPreview(taskID) { async function getPreview(taskID) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Just go to server and get preview (no any cache) // Just go to server and get preview (no any cache)
serverProxy.frames.getPreview(taskID).then((result) => { serverProxy.frames
if (isNode) { .getPreview(taskID)
resolve(global.Buffer.from(result, 'binary').toString('base64')); .then((result) => {
} else if (isBrowser) { if (isNode) {
const reader = new FileReader(); // eslint-disable-next-line no-undef
reader.onload = () => { resolve(global.Buffer.from(result, 'binary').toString('base64'));
resolve(reader.result); } else if (isBrowser) {
}; const reader = new FileReader();
reader.readAsDataURL(result); reader.onload = () => {
} resolve(reader.result);
}).catch((error) => { };
reject(error); reader.readAsDataURL(result);
}); }
})
.catch((error) => {
reject(error);
});
}); });
} }
async function getFrame(taskID, chunkSize, chunkType, mode, frame, async function getFrame(taskID, chunkSize, chunkType, mode, frame, startFrame, stopFrame, isPlaying, step) {
startFrame, stopFrame, isPlaying, step) {
if (!(taskID in frameDataCache)) { if (!(taskID in frameDataCache)) {
const blockType = chunkType === 'video' ? cvatData.BlockType.MP4VIDEO const blockType = chunkType === 'video' ? cvatData.BlockType.MP4VIDEO : cvatData.BlockType.ARCHIVE;
: cvatData.BlockType.ARCHIVE;
const meta = await serverProxy.frames.getMeta(taskID); const meta = await serverProxy.frames.getMeta(taskID);
const mean = meta.frames.reduce((a, b) => a + b.width * b.height, 0) const mean = meta.frames.reduce((a, b) => a + b.width * b.height, 0) / meta.frames.length;
/ meta.frames.length; const stdDev = Math.sqrt(
const stdDev = Math.sqrt(meta.frames.map( meta.frames.map((x) => Math.pow(x.width * x.height - mean, 2)).reduce((a, b) => a + b)
(x) => Math.pow(x.width * x.height - mean, 2), / meta.frames.length,
).reduce((a, b) => a + b) / meta.frames.length); );
// limit of decoded frames cache by 2GB // limit of decoded frames cache by 2GB
const decodedBlocksCacheSize = Math.floor(2147483648 / (mean + stdDev) / 4 / chunkSize) const decodedBlocksCacheSize = Math.floor(2147483648 / (mean + stdDev) / 4 / chunkSize) || 1;
|| 1;
frameDataCache[taskID] = { frameDataCache[taskID] = {
meta, meta,
@ -570,8 +579,11 @@
startFrame, startFrame,
stopFrame, stopFrame,
provider: new cvatData.FrameProvider( provider: new cvatData.FrameProvider(
blockType, chunkSize, Math.max(decodedBlocksCacheSize, 9), blockType,
decodedBlocksCacheSize, 1, chunkSize,
Math.max(decodedBlocksCacheSize, 9),
decodedBlocksCacheSize,
1,
), ),
frameBuffer: new FrameBuffer( frameBuffer: new FrameBuffer(
Math.min(180, decodedBlocksCacheSize * chunkSize), Math.min(180, decodedBlocksCacheSize * chunkSize),

@ -0,0 +1,335 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
const quickhull = require('quickhull');
const PluginRegistry = require('./plugins');
const Comment = require('./comment');
const User = require('./user');
const { ArgumentError } = require('./exceptions');
const { negativeIDGenerator } = require('./common');
const serverProxy = require('./server-proxy');
/**
* Class representing a single issue
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Issue {
constructor(initialData) {
const data = {
id: undefined,
position: undefined,
comment_set: [],
frame: undefined,
created_date: undefined,
resolved_date: undefined,
owner: undefined,
resolver: undefined,
removed: false,
};
for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
data[property] = initialData[property];
}
}
if (data.owner && !(data.owner instanceof User)) data.owner = new User(data.owner);
if (data.resolver && !(data.resolver instanceof User)) data.resolver = new User(data.resolver);
if (data.comment_set) {
data.comment_set = data.comment_set.map((comment) => new Comment(comment));
}
if (typeof data.id === 'undefined') {
data.id = negativeIDGenerator();
}
if (typeof data.created_date === 'undefined') {
data.created_date = new Date().toISOString();
}
Object.defineProperties(
this,
Object.freeze({
/**
* @name id
* @type {integer}
* @memberof module:API.cvat.classes.Issue
* @readonly
* @instance
*/
id: {
get: () => data.id,
},
/**
* Region of interests of the issue
* @name position
* @type {number[]}
* @memberof module:API.cvat.classes.Issue
* @instance
* @readonly
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
position: {
get: () => data.position,
set: (value) => {
if (Array.isArray(value) || value.some((coord) => typeof coord !== 'number')) {
throw new ArgumentError(`Array of numbers is expected. Got ${value}`);
}
data.position = value;
},
},
/**
* List of comments attached to the issue
* @name comments
* @type {module:API.cvat.classes.Comment[]}
* @memberof module:API.cvat.classes.Issue
* @instance
* @readonly
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
comments: {
get: () => data.comment_set.filter((comment) => !comment.removed),
},
/**
* @name frame
* @type {integer}
* @memberof module:API.cvat.classes.Issue
* @readonly
* @instance
*/
frame: {
get: () => data.frame,
},
/**
* @name createdDate
* @type {string}
* @memberof module:API.cvat.classes.Issue
* @readonly
* @instance
*/
createdDate: {
get: () => data.created_date,
},
/**
* @name resolvedDate
* @type {string}
* @memberof module:API.cvat.classes.Issue
* @readonly
* @instance
*/
resolvedDate: {
get: () => data.resolved_date,
},
/**
* An instance of a user who has raised the issue
* @name owner
* @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Issue
* @readonly
* @instance
*/
owner: {
get: () => data.owner,
},
/**
* An instance of a user who has resolved the issue
* @name resolver
* @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Issue
* @readonly
* @instance
*/
resolver: {
get: () => data.resolver,
},
/**
* @name removed
* @type {boolean}
* @memberof module:API.cvat.classes.Comment
* @instance
*/
removed: {
get: () => data.removed,
set: (value) => {
if (typeof value !== 'boolean') {
throw new ArgumentError('Value must be a boolean value');
}
data.removed = value;
},
},
__internal: {
get: () => data,
},
}),
);
}
static hull(coordinates) {
if (coordinates.length > 4) {
const points = coordinates.reduce((acc, coord, index, arr) => {
if (index % 2) acc.push({ x: arr[index - 1], y: coord });
return acc;
}, []);
return quickhull(points)
.map((point) => [point.x, point.y])
.flat();
}
return coordinates;
}
/**
* @typedef {Object} CommentData
* @property {number} [author] an ID of a user who has created the comment
* @property {string} message a comment message
* @global
*/
/**
* Method appends a comment to the issue
* For a new issue it saves comment locally, for a saved issue it saves comment on the server
* @method comment
* @memberof module:API.cvat.classes.Issue
* @param {CommentData} data
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
async comment(data) {
const result = await PluginRegistry.apiWrapper.call(this, Issue.prototype.comment, data);
return result;
}
/**
* The method resolves the issue
* New issues are resolved locally, server-saved issues are resolved on the server
* @method resolve
* @memberof module:API.cvat.classes.Issue
* @param {module:API.cvat.classes.User} user
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
async resolve(user) {
const result = await PluginRegistry.apiWrapper.call(this, Issue.prototype.resolve, user);
return result;
}
/**
* The method resolves the issue
* New issues are reopened locally, server-saved issues are reopened on the server
* @method reopen
* @memberof module:API.cvat.classes.Issue
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
async reopen() {
const result = await PluginRegistry.apiWrapper.call(this, Issue.prototype.reopen);
return result;
}
serialize() {
const { comments } = this;
const data = {
position: this.position,
frame: this.frame,
comment_set: comments.map((comment) => comment.serialize()),
};
if (this.id > 0) {
data.id = this.id;
}
if (this.createdDate) {
data.created_date = this.createdDate;
}
if (this.resolvedDate) {
data.resolved_date = this.resolvedDate;
}
if (this.owner) {
data.owner = this.owner.toJSON();
}
if (this.resolver) {
data.resolver = this.resolver.toJSON();
}
return data;
}
toJSON() {
const data = this.serialize();
const { owner, resolver, ...updated } = data;
return {
...updated,
comment_set: this.comments.map((comment) => comment.toJSON()),
owner_id: owner ? owner.id : undefined,
resolver_id: resolver ? resolver.id : undefined,
};
}
}
Issue.prototype.comment.implementation = async function (data) {
if (typeof data !== 'object' || data === null) {
throw new ArgumentError(`The argument "data" must be a not null object. Got ${data}`);
}
if (typeof data.message !== 'string' || data.message.length < 1) {
throw new ArgumentError(`Comment message must be a not empty string. Got ${data.message}`);
}
if (!(data.author instanceof User)) {
throw new ArgumentError(`Author of the comment must a User instance. Got ${data.author}`);
}
const comment = new Comment(data);
const { id } = this;
if (id >= 0) {
const jsonified = comment.toJSON();
jsonified.issue = id;
const response = await serverProxy.comments.create(jsonified);
const savedComment = new Comment(response);
this.__internal.comment_set.push(savedComment);
} else {
this.__internal.comment_set.push(comment);
}
};
Issue.prototype.resolve.implementation = async function (user) {
if (!(user instanceof User)) {
throw new ArgumentError(`The argument "user" must be an instance of a User class. Got ${typeof user}`);
}
const { id } = this;
if (id >= 0) {
const response = await serverProxy.issues.update(id, { resolver_id: user.id });
this.__internal.resolved_date = response.resolved_date;
this.__internal.resolver = new User(response.resolver);
} else {
this.__internal.resolved_date = new Date().toISOString();
this.__internal.resolver = user;
}
};
Issue.prototype.reopen.implementation = async function () {
const { id } = this;
if (id >= 0) {
const response = await serverProxy.issues.update(id, { resolver_id: null });
this.__internal.resolved_date = response.resolved_date;
this.__internal.resolver = response.resolver;
} else {
this.__internal.resolved_date = null;
this.__internal.resolver = null;
}
};
module.exports = Issue;

@ -1,23 +1,16 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
(() => { (() => {
const { const { AttributeType } = require('./enums');
AttributeType,
} = require('./enums');
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
/** /**
* Class representing an attribute * Class representing an attribute
* @memberof module:API.cvat.classes * @memberof module:API.cvat.classes
* @hideconstructor * @hideconstructor
*/ */
class Attribute { class Attribute {
constructor(initialData) { constructor(initialData) {
const data = { const data = {
@ -42,73 +35,74 @@
} }
if (!Object.values(AttributeType).includes(data.input_type)) { if (!Object.values(AttributeType).includes(data.input_type)) {
throw new ArgumentError( throw new ArgumentError(`Got invalid attribute type ${data.input_type}`);
`Got invalid attribute type ${data.input_type}`,
);
} }
Object.defineProperties(this, Object.freeze({ Object.defineProperties(
/** this,
* @name id Object.freeze({
* @type {integer} /**
* @memberof module:API.cvat.classes.Attribute * @name id
* @readonly * @type {integer}
* @instance * @memberof module:API.cvat.classes.Attribute
*/ * @readonly
id: { * @instance
get: () => data.id, */
}, id: {
/** get: () => data.id,
* @name defaultValue },
* @type {(string|integer|boolean)} /**
* @memberof module:API.cvat.classes.Attribute * @name defaultValue
* @readonly * @type {(string|integer|boolean)}
* @instance * @memberof module:API.cvat.classes.Attribute
*/ * @readonly
defaultValue: { * @instance
get: () => data.default_value, */
}, defaultValue: {
/** get: () => data.default_value,
* @name inputType },
* @type {module:API.cvat.enums.AttributeType} /**
* @memberof module:API.cvat.classes.Attribute * @name inputType
* @readonly * @type {module:API.cvat.enums.AttributeType}
* @instance * @memberof module:API.cvat.classes.Attribute
*/ * @readonly
inputType: { * @instance
get: () => data.input_type, */
}, inputType: {
/** get: () => data.input_type,
* @name mutable },
* @type {boolean} /**
* @memberof module:API.cvat.classes.Attribute * @name mutable
* @readonly * @type {boolean}
* @instance * @memberof module:API.cvat.classes.Attribute
*/ * @readonly
mutable: { * @instance
get: () => data.mutable, */
}, mutable: {
/** get: () => data.mutable,
* @name name },
* @type {string} /**
* @memberof module:API.cvat.classes.Attribute * @name name
* @readonly * @type {string}
* @instance * @memberof module:API.cvat.classes.Attribute
*/ * @readonly
name: { * @instance
get: () => data.name, */
}, name: {
/** get: () => data.name,
* @name values },
* @type {(string[]|integer[]|boolean[])} /**
* @memberof module:API.cvat.classes.Attribute * @name values
* @readonly * @type {(string[]|integer[]|boolean[])}
* @instance * @memberof module:API.cvat.classes.Attribute
*/ * @readonly
values: { * @instance
get: () => [...data.values], */
}, values: {
})); get: () => [...data.values],
},
}),
);
} }
toJSON() { toJSON() {
@ -120,7 +114,7 @@
values: this.values, values: this.values,
}; };
if (typeof (this.id) !== 'undefined') { if (typeof this.id !== 'undefined') {
object.id = this.id; object.id = this.id;
} }
@ -129,10 +123,10 @@
} }
/** /**
* Class representing a label * Class representing a label
* @memberof module:API.cvat.classes * @memberof module:API.cvat.classes
* @hideconstructor * @hideconstructor
*/ */
class Label { class Label {
constructor(initialData) { constructor(initialData) {
const data = { const data = {
@ -151,62 +145,67 @@
data.attributes = []; data.attributes = [];
if (Object.prototype.hasOwnProperty.call(initialData, 'attributes') if (
&& Array.isArray(initialData.attributes)) { Object.prototype.hasOwnProperty.call(initialData, 'attributes')
&& Array.isArray(initialData.attributes)
) {
for (const attrData of initialData.attributes) { for (const attrData of initialData.attributes) {
data.attributes.push(new Attribute(attrData)); data.attributes.push(new Attribute(attrData));
} }
} }
Object.defineProperties(this, Object.freeze({ Object.defineProperties(
/** this,
* @name id Object.freeze({
* @type {integer} /**
* @memberof module:API.cvat.classes.Label * @name id
* @readonly * @type {integer}
* @instance * @memberof module:API.cvat.classes.Label
*/ * @readonly
id: { * @instance
get: () => data.id, */
}, id: {
/** get: () => data.id,
* @name name },
* @type {string} /**
* @memberof module:API.cvat.classes.Label * @name name
* @readonly * @type {string}
* @instance * @memberof module:API.cvat.classes.Label
*/ * @readonly
name: { * @instance
get: () => data.name, */
}, name: {
/** get: () => data.name,
* @name color },
* @type {string} /**
* @memberof module:API.cvat.classes.Label * @name color
* @readonly * @type {string}
* @instance * @memberof module:API.cvat.classes.Label
*/ * @readonly
color: { * @instance
get: () => data.color, */
set: (color) => { color: {
if (typeof color === 'string' && color.match(/^#[0-9a-f]{6}$|^$/)) { get: () => data.color,
data.color = color; set: (color) => {
} else { if (typeof color === 'string' && color.match(/^#[0-9a-f]{6}$|^$/)) {
throw new ArgumentError('Trying to set wrong color format'); data.color = color;
} } else {
throw new ArgumentError('Trying to set wrong color format');
}
},
},
/**
* @name attributes
* @type {module:API.cvat.classes.Attribute[]}
* @memberof module:API.cvat.classes.Label
* @readonly
* @instance
*/
attributes: {
get: () => [...data.attributes],
}, },
}, }),
/** );
* @name attributes
* @type {module:API.cvat.classes.Attribute[]}
* @memberof module:API.cvat.classes.Label
* @readonly
* @instance
*/
attributes: {
get: () => [...data.attributes],
},
}));
} }
toJSON() { toJSON() {
@ -216,7 +215,7 @@
color: this.color, color: this.color,
}; };
if (typeof (this.id) !== 'undefined') { if (typeof this.id !== 'undefined') {
object.id = this.id; object.id = this.id;
} }

@ -1,11 +1,6 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2020 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy');
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
@ -28,14 +23,12 @@ class LambdaManager {
const models = []; const models = [];
for (const model of result) { for (const model of result) {
models.push(new MLModel({ models.push(
id: model.id, new MLModel({
name: model.name, ...model,
description: model.description, type: model.kind,
framework: model.framework, }),
labels: [...model.labels], );
type: model.kind,
}));
} }
this.cachedList = models; this.cachedList = models;
@ -45,20 +38,18 @@ class LambdaManager {
async run(task, model, args) { async run(task, model, args) {
if (!(task instanceof Task)) { if (!(task instanceof Task)) {
throw new ArgumentError( throw new ArgumentError(
`Argument task is expected to be an instance of Task class, but got ${typeof (task)}`, `Argument task is expected to be an instance of Task class, but got ${typeof task}`,
); );
} }
if (!(model instanceof MLModel)) { if (!(model instanceof MLModel)) {
throw new ArgumentError( throw new ArgumentError(
`Argument model is expected to be an instance of MLModel class, but got ${typeof (model)}`, `Argument model is expected to be an instance of MLModel class, but got ${typeof model}`,
); );
} }
if (args && typeof (args) !== 'object') { if (args && typeof args !== 'object') {
throw new ArgumentError( throw new ArgumentError(`Argument args is expected to be an object, but got ${typeof model}`);
`Argument args is expected to be an object, but got ${typeof (model)}`,
);
} }
const body = args; const body = args;
@ -82,7 +73,7 @@ class LambdaManager {
} }
async cancel(requestID) { async cancel(requestID) {
if (typeof (requestID) !== 'string') { if (typeof requestID !== 'string') {
throw new ArgumentError(`Request id argument is required to be a string. But got ${requestID}`); throw new ArgumentError(`Request id argument is required to be a string. But got ${requestID}`);
} }
@ -112,7 +103,11 @@ class LambdaManager {
delete this.listening[requestID]; delete this.listening[requestID];
} }
} catch (error) { } catch (error) {
onUpdate(RQStatus.UNKNOWN, 0, `Could not get a status of the request ${requestID}. ${error.toString()}`); onUpdate(
RQStatus.UNKNOWN,
0,
`Could not get a status of the request ${requestID}. ${error.toString()}`,
);
} }
}; };

@ -1,21 +1,17 @@
// Copyright (C) 2020 Intel Corporation // Copyright (C) 2019-2020 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
/* global
require:false
*/
const { detect } = require('detect-browser'); const { detect } = require('detect-browser');
const PluginRegistry = require('./plugins'); const PluginRegistry = require('./plugins');
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
const { LogType } = require('./enums'); const { LogType } = require('./enums');
/** /**
* Class representing a single log * Class representing a single log
* @memberof module:API.cvat.classes * @memberof module:API.cvat.classes
* @hideconstructor * @hideconstructor
*/ */
class Log { class Log {
constructor(logType, payload) { constructor(logType, payload) {
this.onCloseCallback = null; this.onCloseCallback = null;
@ -30,7 +26,7 @@ class Log {
} }
validatePayload() { validatePayload() {
if (typeof (this.payload) !== 'object') { if (typeof this.payload !== 'object') {
throw new ArgumentError('Payload must be an object'); throw new ArgumentError('Payload must be an object');
} }
@ -63,22 +59,21 @@ class Log {
} }
/** /**
* Method saves a durable log in a storage <br> * Method saves a durable log in a storage <br>
* Note then you can call close() multiple times <br> * Note then you can call close() multiple times <br>
* Log duration will be computed based on the latest call <br> * Log duration will be computed based on the latest call <br>
* All payloads will be shallowly combined (all top level properties will exist) * All payloads will be shallowly combined (all top level properties will exist)
* @method close * @method close
* @memberof module:API.cvat.classes.Log * @memberof module:API.cvat.classes.Log
* @param {object} [payload] part of payload can be added when close a log * @param {object} [payload] part of payload can be added when close a log
* @readonly * @readonly
* @instance * @instance
* @async * @async
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError} * @throws {module:API.cvat.exceptions.ArgumentError}
*/ */
async close(payload = {}) { async close(payload = {}) {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper.call(this, Log.prototype.close, payload);
.apiWrapper.call(this, Log.prototype.close, payload);
return result; return result;
} }
} }
@ -96,8 +91,7 @@ class LogWithCount extends Log {
validatePayload() { validatePayload() {
Log.prototype.validatePayload.call(this); Log.prototype.validatePayload.call(this);
if (!Number.isInteger(this.payload.count) || this.payload.count < 1) { if (!Number.isInteger(this.payload.count) || this.payload.count < 1) {
const message = `The field "count" is required for "${this.type}" log` const message = `The field "count" is required for "${this.type}" log. It must be a positive integer`;
+ 'It must be a positive integer';
throw new ArgumentError(message); throw new ArgumentError(message);
} }
} }
@ -148,12 +142,14 @@ class LogWithWorkingTime extends Log {
validatePayload() { validatePayload() {
Log.prototype.validatePayload.call(this); Log.prototype.validatePayload.call(this);
if (!('working_time' in this.payload) if (
|| !typeof (this.payload.working_time) === 'number' !('working_time' in this.payload)
|| !typeof this.payload.working_time === 'number'
|| this.payload.working_time < 0 || this.payload.working_time < 0
) { ) {
const message = `The field "working_time" is required for ${this.type} log. ` const message = `
+ 'It must be a number not less than 0'; The field "working_time" is required for ${this.type} log. It must be a number not less than 0
`;
throw new ArgumentError(message); throw new ArgumentError(message);
} }
} }
@ -163,40 +159,35 @@ class LogWithExceptionInfo extends Log {
validatePayload() { validatePayload() {
Log.prototype.validatePayload.call(this); Log.prototype.validatePayload.call(this);
if (typeof (this.payload.message) !== 'string') { if (typeof this.payload.message !== 'string') {
const message = `The field "message" is required for ${this.type} log. ` const message = `The field "message" is required for ${this.type} log. It must be a string`;
+ 'It must be a string';
throw new ArgumentError(message); throw new ArgumentError(message);
} }
if (typeof (this.payload.filename) !== 'string') { if (typeof this.payload.filename !== 'string') {
const message = `The field "filename" is required for ${this.type} log. ` const message = `The field "filename" is required for ${this.type} log. It must be a string`;
+ 'It must be a string';
throw new ArgumentError(message); throw new ArgumentError(message);
} }
if (typeof (this.payload.line) !== 'number') { if (typeof this.payload.line !== 'number') {
const message = `The field "line" is required for ${this.type} log. ` const message = `The field "line" is required for ${this.type} log. It must be a number`;
+ 'It must be a number';
throw new ArgumentError(message); throw new ArgumentError(message);
} }
if (typeof (this.payload.column) !== 'number') { if (typeof this.payload.column !== 'number') {
const message = `The field "column" is required for ${this.type} log. ` const message = `The field "column" is required for ${this.type} log. It must be a number`;
+ 'It must be a number';
throw new ArgumentError(message); throw new ArgumentError(message);
} }
if (typeof (this.payload.stack) !== 'string') { if (typeof this.payload.stack !== 'string') {
const message = `The field "stack" is required for ${this.type} log. ` const message = `The field "stack" is required for ${this.type} log. It must be a string`;
+ 'It must be a string';
throw new ArgumentError(message); throw new ArgumentError(message);
} }
} }
dump() { dump() {
let body = super.dump(); let body = super.dump();
const payload = body.payload; const { payload } = body;
const client = detect(); const client = detect();
body = { body = {
...body, ...body,
@ -222,8 +213,11 @@ class LogWithExceptionInfo extends Log {
function logFactory(logType, payload) { function logFactory(logType, payload) {
const logsWithCount = [ const logsWithCount = [
LogType.deleteObject, LogType.mergeObjects, LogType.copyObject, LogType.deleteObject,
LogType.undoAction, LogType.redoAction, LogType.mergeObjects,
LogType.copyObject,
LogType.undoAction,
LogType.redoAction,
]; ];
if (logsWithCount.includes(logType)) { if (logsWithCount.includes(logType)) {

@ -1,11 +1,7 @@
// Copyright (C) 2020 Intel Corporation // Copyright (C) 2019-2020 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
/* global
require:false
*/
const PluginRegistry = require('./plugins'); const PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy');
const logFactory = require('./log'); const logFactory = require('./log');
@ -14,6 +10,12 @@ const { LogType } = require('./enums');
const WORKING_TIME_THRESHOLD = 100000; // ms, 1.66 min const WORKING_TIME_THRESHOLD = 100000; // ms, 1.66 min
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
class LoggerStorage { class LoggerStorage {
constructor() { constructor() {
this.clientID = Date.now().toString().substr(-6); this.clientID = Date.now().toString().substr(-6);
@ -22,6 +24,7 @@ class LoggerStorage {
this.collection = []; this.collection = [];
this.ignoreRules = {}; // by event this.ignoreRules = {}; // by event
this.isActiveChecker = null; this.isActiveChecker = null;
this.saving = false;
this.ignoreRules[LogType.zoomImage] = { this.ignoreRules[LogType.zoomImage] = {
lastLog: null, lastLog: null,
@ -34,8 +37,10 @@ class LoggerStorage {
this.ignoreRules[LogType.changeAttribute] = { this.ignoreRules[LogType.changeAttribute] = {
lastLog: null, lastLog: null,
ignore(previousLog, currentPayload) { ignore(previousLog, currentPayload) {
return currentPayload.object_id === previousLog.payload.object_id return (
&& currentPayload.id === previousLog.payload.id; currentPayload.object_id === previousLog.payload.object_id
&& currentPayload.id === previousLog.payload.id
);
}, },
}; };
} }
@ -50,32 +55,28 @@ class LoggerStorage {
} }
async configure(isActiveChecker, activityHelper) { async configure(isActiveChecker, activityHelper) {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper.call(
.apiWrapper.call( this,
this, LoggerStorage.prototype.configure, LoggerStorage.prototype.configure,
isActiveChecker, activityHelper, isActiveChecker,
); activityHelper,
);
return result; return result;
} }
async log(logType, payload = {}, wait = false) { async log(logType, payload = {}, wait = false) {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper.call(this, LoggerStorage.prototype.log, logType, payload, wait);
.apiWrapper.call(this, LoggerStorage.prototype.log, logType, payload, wait);
return result; return result;
} }
async save() { async save() {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper.call(this, LoggerStorage.prototype.save);
.apiWrapper.call(this, LoggerStorage.prototype.save);
return result; return result;
} }
} }
LoggerStorage.prototype.configure.implementation = function ( LoggerStorage.prototype.configure.implementation = function (isActiveChecker, userActivityCallback) {
isActiveChecker, if (typeof isActiveChecker !== 'function') {
userActivityCallback,
) {
if (typeof (isActiveChecker) !== 'function') {
throw new ArgumentError('isActiveChecker argument must be callable'); throw new ArgumentError('isActiveChecker argument must be callable');
} }
@ -88,11 +89,11 @@ LoggerStorage.prototype.configure.implementation = function (
}; };
LoggerStorage.prototype.log.implementation = function (logType, payload, wait) { LoggerStorage.prototype.log.implementation = function (logType, payload, wait) {
if (typeof (payload) !== 'object') { if (typeof payload !== 'object') {
throw new ArgumentError('Payload must be an object'); throw new ArgumentError('Payload must be an object');
} }
if (typeof (wait) !== 'boolean') { if (typeof wait !== 'boolean') {
throw new ArgumentError('Payload must be an object'); throw new ArgumentError('Payload must be an object');
} }
@ -146,6 +147,10 @@ LoggerStorage.prototype.log.implementation = function (logType, payload, wait) {
}; };
LoggerStorage.prototype.save.implementation = async function () { LoggerStorage.prototype.save.implementation = async function () {
while (this.saving) {
await sleep(100);
}
const collectionToSend = [...this.collection]; const collectionToSend = [...this.collection];
const lastLog = this.collection[this.collection.length - 1]; const lastLog = this.collection[this.collection.length - 1];
@ -164,14 +169,18 @@ LoggerStorage.prototype.save.implementation = async function () {
const userActivityLog = logFactory(LogType.sendUserActivity, logPayload); const userActivityLog = logFactory(LogType.sendUserActivity, logPayload);
collectionToSend.push(userActivityLog); collectionToSend.push(userActivityLog);
await serverProxy.logs.save(collectionToSend.map((log) => log.dump())); try {
this.saving = true;
for (const rule of Object.values(this.ignoreRules)) { await serverProxy.logs.save(collectionToSend.map((log) => log.dump()));
rule.lastLog = null; for (const rule of Object.values(this.ignoreRules)) {
rule.lastLog = null;
}
this.collection = [];
this.workingTime = 0;
this.lastLogTime = Date.now();
} finally {
this.saving = false;
} }
this.collection = [];
this.workingTime = 0;
this.lastLogTime = Date.now();
}; };
module.exports = new LoggerStorage(); module.exports = new LoggerStorage();

@ -1,12 +1,11 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019-2020 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
/** /**
* Class representing a machine learning model * Class representing a machine learning model
* @memberof module:API.cvat.classes * @memberof module:API.cvat.classes
*/ */
class MLModel { class MLModel {
constructor(data) { constructor(data) {
this._id = data.id; this._id = data.id;
@ -15,12 +14,17 @@ class MLModel {
this._framework = data.framework; this._framework = data.framework;
this._description = data.description; this._description = data.description;
this._type = data.type; this._type = data.type;
this._params = {
canvas: {
minPosVertices: data.min_pos_points,
},
};
} }
/** /**
* @returns {string} * @returns {string}
* @readonly * @readonly
*/ */
get id() { get id() {
return this._id; return this._id;
} }
@ -28,7 +32,7 @@ class MLModel {
/** /**
* @returns {string} * @returns {string}
* @readonly * @readonly
*/ */
get name() { get name() {
return this._name; return this._name;
} }
@ -36,7 +40,7 @@ class MLModel {
/** /**
* @returns {string[]} * @returns {string[]}
* @readonly * @readonly
*/ */
get labels() { get labels() {
if (Array.isArray(this._labels)) { if (Array.isArray(this._labels)) {
return [...this._labels]; return [...this._labels];
@ -48,7 +52,7 @@ class MLModel {
/** /**
* @returns {string} * @returns {string}
* @readonly * @readonly
*/ */
get framework() { get framework() {
return this._framework; return this._framework;
} }
@ -56,7 +60,7 @@ class MLModel {
/** /**
* @returns {string} * @returns {string}
* @readonly * @readonly
*/ */
get description() { get description() {
return this._description; return this._description;
} }
@ -64,10 +68,20 @@ class MLModel {
/** /**
* @returns {module:API.cvat.enums.ModelType} * @returns {module:API.cvat.enums.ModelType}
* @readonly * @readonly
*/ */
get type() { get type() {
return this._type; return this._type;
} }
/**
* @returns {object}
* @readonly
*/
get params() {
return {
canvas: { ...this._params.canvas },
};
}
} }
module.exports = MLModel; module.exports = MLModel;

@ -1,31 +1,26 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
const { Source } = require('./enums'); const { Source } = require('./enums');
/* global
require:false
*/
(() => { (() => {
const PluginRegistry = require('./plugins'); const PluginRegistry = require('./plugins');
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
/** /**
* Class representing a state of an object on a specific frame * Class representing a state of an object on a specific frame
* @memberof module:API.cvat.classes * @memberof module:API.cvat.classes
*/ */
class ObjectState { class ObjectState {
/** /**
* @param {Object} serialized - is an dictionary which contains * @param {Object} serialized - is an dictionary which contains
* initial information about an ObjectState; * initial information about an ObjectState;
* </br> Necessary fields: objectType, shapeType, frame, updated, group * </br> Necessary fields: objectType, shapeType, frame, updated, group
* </br> Optional fields: keyframes, clientID, serverID * </br> Optional fields: keyframes, clientID, serverID
* </br> Optional fields which can be set later: points, zOrder, outside, * </br> Optional fields which can be set later: points, zOrder, outside,
* occluded, hidden, attributes, lock, label, color, keyframe, source * occluded, hidden, attributes, lock, label, color, keyframe, source
*/ */
constructor(serialized) { constructor(serialized) {
const data = { const data = {
label: null, label: null,
@ -77,282 +72,289 @@ const { Source } = require('./enums');
writable: false, writable: false,
}); });
Object.defineProperties(this, Object.freeze({ Object.defineProperties(
// Internal property. We don't need document it. this,
updateFlags: { Object.freeze({
get: () => data.updateFlags, // Internal property. We don't need document it.
}, updateFlags: {
frame: { get: () => data.updateFlags,
/**
* @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,
},
source: {
/**
* @name source
* @type {module:API.cvat.enums.Source}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => data.source,
},
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;
}, },
}, frame: {
color: { /**
/** * @name frame
* @name color * @type {integer}
* @type {string} * @memberof module:API.cvat.classes.ObjectState
* @memberof module:API.cvat.classes.ObjectState * @readonly
* @instance * @instance
*/ */
get: () => data.color, get: () => data.frame,
set: (color) => {
data.updateFlags.color = true;
data.color = color;
}, },
}, objectType: {
hidden: { /**
/** * @name objectType
* @name hidden * @type {module:API.cvat.enums.ObjectType}
* @type {boolean} * @memberof module:API.cvat.classes.ObjectState
* @memberof module:API.cvat.classes.ObjectState * @readonly
* @instance * @instance
*/ */
get: () => data.hidden, get: () => data.objectType,
set: (hidden) => {
data.updateFlags.hidden = true;
data.hidden = hidden;
}, },
}, shapeType: {
points: { /**
/** * @name shapeType
* @name points * @type {module:API.cvat.enums.ObjectShape}
* @type {number[]} * @memberof module:API.cvat.classes.ObjectState
* @memberof module:API.cvat.classes.ObjectState * @readonly
* @throws {module:API.cvat.exceptions.ArgumentError} * @instance
* @instance */
*/ get: () => data.shapeType,
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)}`,
);
}
}, },
}, source: {
group: { /**
/** * @name source
* Object with short group info { color, id } * @type {module:API.cvat.enums.Source}
* @name group * @memberof module:API.cvat.classes.ObjectState
* @type {object} * @readonly
* @memberof module:API.cvat.classes.ObjectState * @instance
* @instance */
* @readonly get: () => data.source,
*/
get: () => data.group,
},
zOrder: {
/**
* @name zOrder
* @type {integer | null}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.zOrder,
set: (zOrder) => {
data.updateFlags.zOrder = true;
data.zOrder = zOrder;
}, },
}, clientID: {
outside: { /**
/** * @name clientID
* @name outside * @type {integer}
* @type {boolean} * @memberof module:API.cvat.classes.ObjectState
* @memberof module:API.cvat.classes.ObjectState * @readonly
* @instance * @instance
*/ */
get: () => data.outside, get: () => data.clientID,
set: (outside) => {
data.updateFlags.outside = true;
data.outside = outside;
}, },
}, serverID: {
keyframe: { /**
/** * @name serverID
* @name keyframe * @type {integer}
* @type {boolean} * @memberof module:API.cvat.classes.ObjectState
* @memberof module:API.cvat.classes.ObjectState * @readonly
* @instance * @instance
*/ */
get: () => data.keyframe, get: () => data.serverID,
set: (keyframe) => {
data.updateFlags.keyframe = true;
data.keyframe = keyframe;
}, },
}, label: {
keyframes: { /**
/** * @name shape
* Object of keyframes { first, prev, next, last } * @type {module:API.cvat.classes.Label}
* @name keyframes * @memberof module:API.cvat.classes.ObjectState
* @type {object | null} * @instance
* @memberof module:API.cvat.classes.ObjectState */
* @readonly get: () => data.label,
* @instance set: (labelInstance) => {
*/ data.updateFlags.label = true;
get: () => { data.label = labelInstance;
if (typeof (data.keyframes) === 'object') { },
return { ...data.keyframes }; },
} color: {
/**
* @name color
* @type {string}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.color,
set: (color) => {
data.updateFlags.color = true;
data.color = color;
},
},
hidden: {
/**
* @name hidden
* @type {boolean}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.hidden,
set: (hidden) => {
data.updateFlags.hidden = true;
data.hidden = hidden;
},
},
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: {
/**
* Object with short group info { color, id }
* @name group
* @type {object}
* @memberof module:API.cvat.classes.ObjectState
* @instance
* @readonly
*/
get: () => data.group,
},
zOrder: {
/**
* @name zOrder
* @type {integer | null}
* @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;
},
},
keyframes: {
/**
* Object of keyframes { first, prev, next, last }
* @name keyframes
* @type {object | null}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => {
if (typeof data.keyframes === 'object') {
return { ...data.keyframes };
}
return null; return null;
},
}, },
}, occluded: {
occluded: { /**
/** * @name occluded
* @name occluded * @type {boolean}
* @type {boolean} * @memberof module:API.cvat.classes.ObjectState
* @memberof module:API.cvat.classes.ObjectState * @instance
* @instance */
*/ get: () => data.occluded,
get: () => data.occluded, set: (occluded) => {
set: (occluded) => { data.updateFlags.occluded = true;
data.updateFlags.occluded = true; data.occluded = occluded;
data.occluded = occluded; },
}, },
}, lock: {
lock: { /**
/** * @name lock
* @name lock * @type {boolean}
* @type {boolean} * @memberof module:API.cvat.classes.ObjectState
* @memberof module:API.cvat.classes.ObjectState * @instance
* @instance */
*/ get: () => data.lock,
get: () => data.lock, set: (lock) => {
set: (lock) => { data.updateFlags.lock = true;
data.updateFlags.lock = true; data.lock = lock;
data.lock = lock; },
}, },
}, pinned: {
pinned: { /**
/** * @name pinned
* @name pinned * @type {boolean | null}
* @type {boolean | null} * @memberof module:API.cvat.classes.ObjectState
* @memberof module:API.cvat.classes.ObjectState * @instance
* @instance */
*/ get: () => {
get: () => { if (typeof data.pinned === 'boolean') {
if (typeof (data.pinned) === 'boolean') { return data.pinned;
return data.pinned; }
}
return null; return null;
},
set: (pinned) => {
data.updateFlags.pinned = true;
data.pinned = pinned;
},
}, },
set: (pinned) => { updated: {
data.updateFlags.pinned = true; /**
data.pinned = pinned; * Timestamp of the latest updated of the object
* @name updated
* @type {number}
* @memberof module:API.cvat.classes.ObjectState
* @instance
* @readonly
*/
get: () => data.updated,
}, },
}, attributes: {
updated: { /**
/** * Object is id:value pairs where "id" is an integer
* Timestamp of the latest updated of the object * attribute identifier and "value" is an attribute value
* @name updated * @name attributes
* @type {number} * @type {Object}
* @memberof module:API.cvat.classes.ObjectState * @memberof module:API.cvat.classes.ObjectState
* @instance * @throws {module:API.cvat.exceptions.ArgumentError}
* @readonly * @instance
*/ */
get: () => data.updated, get: () => data.attributes,
}, set: (attributes) => {
attributes: { if (typeof attributes !== 'object') {
/** throw new ArgumentError(
* Object is id:value pairs where "id" is an integer 'Attributes are expected to be an object '
* attribute identifier and "value" is an attribute value + `but got ${
* @name attributes typeof attributes === 'object'
* @type {Object} ? attributes.constructor.name
* @memberof module:API.cvat.classes.ObjectState : typeof attributes
* @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)) { for (const attrID of Object.keys(attributes)) {
data.updateFlags.attributes = true; data.updateFlags.attributes = true;
data.attributes[attrID] = attributes[attrID]; data.attributes[attrID] = attributes[attrID];
} }
},
}, },
}, }),
})); );
this.label = serialized.label; this.label = serialized.label;
this.lock = serialized.lock; this.lock = serialized.lock;
@ -360,31 +362,31 @@ const { Source } = require('./enums');
if ([Source.MANUAL, Source.AUTO].includes(serialized.source)) { if ([Source.MANUAL, Source.AUTO].includes(serialized.source)) {
data.source = serialized.source; data.source = serialized.source;
} }
if (typeof (serialized.zOrder) === 'number') { if (typeof serialized.zOrder === 'number') {
this.zOrder = serialized.zOrder; this.zOrder = serialized.zOrder;
} }
if (typeof (serialized.occluded) === 'boolean') { if (typeof serialized.occluded === 'boolean') {
this.occluded = serialized.occluded; this.occluded = serialized.occluded;
} }
if (typeof (serialized.outside) === 'boolean') { if (typeof serialized.outside === 'boolean') {
this.outside = serialized.outside; this.outside = serialized.outside;
} }
if (typeof (serialized.keyframe) === 'boolean') { if (typeof serialized.keyframe === 'boolean') {
this.keyframe = serialized.keyframe; this.keyframe = serialized.keyframe;
} }
if (typeof (serialized.pinned) === 'boolean') { if (typeof serialized.pinned === 'boolean') {
this.pinned = serialized.pinned; this.pinned = serialized.pinned;
} }
if (typeof (serialized.hidden) === 'boolean') { if (typeof serialized.hidden === 'boolean') {
this.hidden = serialized.hidden; this.hidden = serialized.hidden;
} }
if (typeof (serialized.color) === 'string') { if (typeof serialized.color === 'string') {
this.color = serialized.color; this.color = serialized.color;
} }
if (Array.isArray(serialized.points)) { if (Array.isArray(serialized.points)) {
this.points = serialized.points; this.points = serialized.points;
} }
if (typeof (serialized.attributes) === 'object') { if (typeof serialized.attributes === 'object') {
this.attributes = serialized.attributes; this.attributes = serialized.attributes;
} }
@ -392,38 +394,36 @@ const { Source } = require('./enums');
} }
/** /**
* Method saves/updates an object state in a collection * Method saves/updates an object state in a collection
* @method save * @method save
* @memberof module:API.cvat.classes.ObjectState * @memberof module:API.cvat.classes.ObjectState
* @readonly * @readonly
* @instance * @instance
* @async * @async
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError} * @throws {module:API.cvat.exceptions.ArgumentError}
* @returns {module:API.cvat.classes.ObjectState} updated state of an object * @returns {module:API.cvat.classes.ObjectState} updated state of an object
*/ */
async save() { async save() {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper.call(this, ObjectState.prototype.save);
.apiWrapper.call(this, ObjectState.prototype.save);
return result; return result;
} }
/** /**
* Method deletes an object from a collection * Method deletes an object from a collection
* @method delete * @method delete
* @memberof module:API.cvat.classes.ObjectState * @memberof module:API.cvat.classes.ObjectState
* @readonly * @readonly
* @instance * @instance
* @param {integer} frame current frame number * @param {integer} frame current frame number
* @param {boolean} [force=false] delete object even if it is locked * @param {boolean} [force=false] delete object even if it is locked
* @async * @async
* @returns {boolean} true if object has been deleted * @returns {boolean} true if object has been deleted
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError} * @throws {module:API.cvat.exceptions.ArgumentError}
*/ */
async delete(frame, force = false) { async delete(frame, force = false) {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper.call(this, ObjectState.prototype.delete, frame, force);
.apiWrapper.call(this, ObjectState.prototype.delete, frame, force);
return result; return result;
} }
} }

@ -1,11 +1,6 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
(() => { (() => {
const { PluginError } = require('./exceptions'); const { PluginError } = require('./exceptions');
@ -16,8 +11,7 @@
// I have to optimize the wrapper // I have to optimize the wrapper
const pluginList = await PluginRegistry.list(); const pluginList = await PluginRegistry.list();
for (const plugin of pluginList) { for (const plugin of pluginList) {
const pluginDecorators = plugin.functions const pluginDecorators = plugin.functions.filter((obj) => obj.callback === wrappedFunc)[0];
.filter((obj) => obj.callback === wrappedFunc)[0];
if (pluginDecorators && pluginDecorators.enter) { if (pluginDecorators && pluginDecorators.enter) {
try { try {
await pluginDecorators.enter.call(this, plugin, ...args); await pluginDecorators.enter.call(this, plugin, ...args);
@ -34,8 +28,7 @@
let result = await wrappedFunc.implementation.call(this, ...args); let result = await wrappedFunc.implementation.call(this, ...args);
for (const plugin of pluginList) { for (const plugin of pluginList) {
const pluginDecorators = plugin.functions const pluginDecorators = plugin.functions.filter((obj) => obj.callback === wrappedFunc)[0];
.filter((obj) => obj.callback === wrappedFunc)[0];
if (pluginDecorators && pluginDecorators.leave) { if (pluginDecorators && pluginDecorators.leave) {
try { try {
result = await pluginDecorators.leave.call(this, plugin, result, ...args); result = await pluginDecorators.leave.call(this, plugin, result, ...args);
@ -56,15 +49,15 @@
static async register(plug) { static async register(plug) {
const functions = []; const functions = [];
if (typeof (plug) !== 'object') { if (typeof plug !== 'object') {
throw new PluginError(`Plugin should be an object, but got "${typeof (plug)}"`); throw new PluginError(`Plugin should be an object, but got "${typeof plug}"`);
} }
if (!('name' in plug) || typeof (plug.name) !== 'string') { if (!('name' in plug) || typeof plug.name !== 'string') {
throw new PluginError('Plugin must contain a "name" field and it must be a string'); throw new PluginError('Plugin must contain a "name" field and it must be a string');
} }
if (!('description' in plug) || typeof (plug.description) !== 'string') { if (!('description' in plug) || typeof plug.description !== 'string') {
throw new PluginError('Plugin must contain a "description" field and it must be a string'); throw new PluginError('Plugin must contain a "description" field and it must be a string');
} }
@ -72,17 +65,19 @@
throw new PluginError('Plugin must not contain a "functions" field'); throw new PluginError('Plugin must not contain a "functions" field');
} }
(function traverse(plugin, api) { function traverse(plugin, api) {
const decorator = {}; const decorator = {};
for (const key in plugin) { for (const key in plugin) {
if (Object.prototype.hasOwnProperty.call(plugin, key)) { if (Object.prototype.hasOwnProperty.call(plugin, key)) {
if (typeof (plugin[key]) === 'object') { if (typeof plugin[key] === 'object') {
if (Object.prototype.hasOwnProperty.call(api, key)) { if (Object.prototype.hasOwnProperty.call(api, key)) {
traverse(plugin[key], api[key]); traverse(plugin[key], api[key]);
} }
} else if (['enter', 'leave'].includes(key) } else if (
&& typeof (api) === 'function' ['enter', 'leave'].includes(key)
&& typeof (plugin[key] === 'function')) { && typeof api === 'function'
&& typeof (plugin[key] === 'function')
) {
decorator.callback = api; decorator.callback = api;
decorator[key] = plugin[key]; decorator[key] = plugin[key];
} }
@ -92,9 +87,9 @@
if (Object.keys(decorator).length) { if (Object.keys(decorator).length) {
functions.push(decorator); functions.push(decorator);
} }
}(plug, { }
cvat: this,
})); traverse(plug, { cvat: this });
Object.defineProperty(plug, 'functions', { Object.defineProperty(plug, 'functions', {
value: functions, value: functions,

@ -0,0 +1,265 @@
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
(() => {
const PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy');
const { ArgumentError } = require('./exceptions');
const { Task } = require('./session');
const { Label } = require('./labels');
const User = require('./user');
/**
* Class representing a project
* @memberof module:API.cvat.classes
*/
class Project {
/**
* In a fact you need use the constructor only if you want to create a project
* @param {object} initialData - Object which is used for initalization
* <br> It can contain keys:
* <br> <li style="margin-left: 10px;"> name
* <br> <li style="margin-left: 10px;"> labels
*/
constructor(initialData) {
const data = {
id: undefined,
name: undefined,
status: undefined,
assignee: undefined,
owner: undefined,
bug_tracker: undefined,
created_date: undefined,
updated_date: undefined,
};
for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
data[property] = initialData[property];
}
}
data.labels = [];
data.tasks = [];
if (Array.isArray(initialData.labels)) {
for (const label of initialData.labels) {
const classInstance = new Label(label);
data.labels.push(classInstance);
}
}
if (Array.isArray(initialData.tasks)) {
for (const task of initialData.tasks) {
const taskInstance = new Task(task);
data.tasks.push(taskInstance);
}
}
Object.defineProperties(
this,
Object.freeze({
/**
* @name id
* @type {integer}
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
*/
id: {
get: () => data.id,
},
/**
* @name name
* @type {string}
* @memberof module:API.cvat.classes.Project
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
name: {
get: () => data.name,
set: (value) => {
if (!value.trim().length) {
throw new ArgumentError('Value must not be empty');
}
data.name = value;
},
},
/**
* @name status
* @type {module:API.cvat.enums.TaskStatus}
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
*/
status: {
get: () => data.status,
},
/**
* Instance of a user who was assigned for the project
* @name assignee
* @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
*/
assignee: {
get: () => data.assignee,
set: (assignee) => {
if (assignee !== null && !(assignee instanceof User)) {
throw new ArgumentError('Value must be a user instance');
}
data.assignee = assignee;
},
},
/**
* Instance of a user who has created the project
* @name owner
* @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
*/
owner: {
get: () => data.owner,
},
/**
* @name bugTracker
* @type {string}
* @memberof module:API.cvat.classes.Project
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
bugTracker: {
get: () => data.bug_tracker,
set: (tracker) => {
data.bug_tracker = tracker;
},
},
/**
* @name createdDate
* @type {string}
* @memberof module:API.cvat.classes.Task
* @readonly
* @instance
*/
createdDate: {
get: () => data.created_date,
},
/**
* @name updatedDate
* @type {string}
* @memberof module:API.cvat.classes.Task
* @readonly
* @instance
*/
updatedDate: {
get: () => data.updated_date,
},
/**
* After project has been created value can be appended only.
* @name labels
* @type {module:API.cvat.classes.Label[]}
* @memberof module:API.cvat.classes.Project
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
labels: {
get: () => [...data.labels],
set: (labels) => {
if (!Array.isArray(labels)) {
throw new ArgumentError('Value must be an array of Labels');
}
if (!Array.isArray(labels) || labels.some((label) => !(label instanceof Label))) {
throw new ArgumentError(
`Each array value must be an instance of Label. ${typeof label} was found`,
);
}
data.labels = [...labels];
},
},
/**
* Tasks linked with the project
* @name tasks
* @type {module:API.cvat.classes.Task[]}
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
*/
tasks: {
get: () => [...data.tasks],
},
}),
);
}
/**
* Method updates data of a created project or creates new project from scratch
* @method save
* @returns {module:API.cvat.classes.Project}
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async save() {
const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.save);
return result;
}
/**
* Method deletes a task from a server
* @method delete
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async delete() {
const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.delete);
return result;
}
}
module.exports = {
Project,
};
Project.prototype.save.implementation = async function () {
if (typeof this.id !== 'undefined') {
const projectData = {
name: this.name,
assignee_id: this.assignee ? this.assignee.id : null,
bug_tracker: this.bugTracker,
labels: [...this.labels.map((el) => el.toJSON())],
};
await serverProxy.projects.save(this.id, projectData);
return this;
}
const projectSpec = {
name: this.name,
labels: [...this.labels.map((el) => el.toJSON())],
};
if (this.bugTracker) {
projectSpec.bug_tracker = this.bugTracker;
}
const project = await serverProxy.projects.create(projectSpec);
return new Project(project);
};
Project.prototype.delete.implementation = async function () {
const result = await serverProxy.projects.delete(this.id);
return result;
};
})();

@ -0,0 +1,397 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
const store = require('store');
const PluginRegistry = require('./plugins');
const Issue = require('./issue');
const User = require('./user');
const { ArgumentError, DataError } = require('./exceptions');
const { ReviewStatus } = require('./enums');
const { negativeIDGenerator } = require('./common');
const serverProxy = require('./server-proxy');
/**
* Class representing a single review
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Review {
constructor(initialData) {
const data = {
id: undefined,
job: undefined,
issue_set: [],
estimated_quality: undefined,
status: undefined,
reviewer: undefined,
assignee: undefined,
reviewed_frames: undefined,
reviewed_states: undefined,
};
for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
data[property] = initialData[property];
}
}
if (data.reviewer && !(data.reviewer instanceof User)) data.reviewer = new User(data.reviewer);
if (data.assignee && !(data.assignee instanceof User)) data.assignee = new User(data.assignee);
data.reviewed_frames = Array.isArray(data.reviewed_frames) ? new Set(data.reviewed_frames) : new Set();
data.reviewed_states = Array.isArray(data.reviewed_states) ? new Set(data.reviewed_states) : new Set();
if (data.issue_set) {
data.issue_set = data.issue_set.map((issue) => new Issue(issue));
}
if (typeof data.id === 'undefined') {
data.id = negativeIDGenerator();
}
Object.defineProperties(
this,
Object.freeze({
/**
* @name id
* @type {integer}
* @memberof module:API.cvat.classes.Review
* @readonly
* @instance
*/
id: {
get: () => data.id,
},
/**
* An identifier of a job the review is attached to
* @name job
* @type {integer}
* @memberof module:API.cvat.classes.Review
* @readonly
* @instance
*/
job: {
get: () => data.job,
},
/**
* List of attached issues
* @name issues
* @type {number[]}
* @memberof module:API.cvat.classes.Review
* @instance
* @readonly
*/
issues: {
get: () => data.issue_set.filter((issue) => !issue.removed),
},
/**
* Estimated quality of the review
* @name estimatedQuality
* @type {number}
* @memberof module:API.cvat.classes.Review
* @instance
* @readonly
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
estimatedQuality: {
get: () => data.estimated_quality,
set: (value) => {
if (typeof value !== 'number' || value < 0 || value > 5) {
throw new ArgumentError(`Value must be a number in range [0, 5]. Got ${value}`);
}
data.estimated_quality = value;
},
},
/**
* @name status
* @type {module:API.cvat.enums.ReviewStatus}
* @memberof module:API.cvat.classes.Review
* @instance
* @readonly
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
status: {
get: () => data.status,
set: (status) => {
const type = ReviewStatus;
let valueInEnum = false;
for (const value in type) {
if (type[value] === status) {
valueInEnum = true;
break;
}
}
if (!valueInEnum) {
throw new ArgumentError(
'Value must be a value from the enumeration cvat.enums.ReviewStatus',
);
}
data.status = status;
},
},
/**
* An instance of a user who has done the review
* @name reviewer
* @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Review
* @readonly
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
reviewer: {
get: () => data.reviewer,
set: (reviewer) => {
if (!(reviewer instanceof User)) {
throw new ArgumentError(`Reviewer must be an instance of the User class. Got ${reviewer}`);
}
data.reviewer = reviewer;
},
},
/**
* An instance of a user who was assigned for annotation before the review
* @name assignee
* @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Review
* @readonly
* @instance
*/
assignee: {
get: () => data.assignee,
},
/**
* A set of frames that have been visited during review
* @name reviewedFrames
* @type {number[]}
* @memberof module:API.cvat.classes.Review
* @readonly
* @instance
*/
reviewedFrames: {
get: () => Array.from(data.reviewed_frames),
},
/**
* A set of reviewed states (server IDs combined with frames)
* @name reviewedFrames
* @type {string[]}
* @memberof module:API.cvat.classes.Review
* @readonly
* @instance
*/
reviewedStates: {
get: () => Array.from(data.reviewed_states),
},
__internal: {
get: () => data,
},
}),
);
}
/**
* Method appends a frame to a set of reviewed frames
* Reviewed frames are saved only in local storage
* @method reviewFrame
* @memberof module:API.cvat.classes.Review
* @param {number} frame
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async reviewFrame(frame) {
const result = await PluginRegistry.apiWrapper.call(this, Review.prototype.reviewFrame, frame);
return result;
}
/**
* Method appends a frame to a set of reviewed frames
* Reviewed states are saved only in local storage. They are used to automatic annotations quality assessment
* @method reviewStates
* @memberof module:API.cvat.classes.Review
* @param {string[]} stateIDs
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async reviewStates(stateIDs) {
const result = await PluginRegistry.apiWrapper.call(this, Review.prototype.reviewStates, stateIDs);
return result;
}
/**
* @typedef {Object} IssueData
* @property {number} frame
* @property {number[]} position
* @property {number} owner
* @property {CommentData[]} comment_set
* @global
*/
/**
* Method adds a new issue to the review
* @method openIssue
* @memberof module:API.cvat.classes.Review
* @param {IssueData} data
* @returns {module:API.cvat.classes.Issue}
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async openIssue(data) {
const result = await PluginRegistry.apiWrapper.call(this, Review.prototype.openIssue, data);
return result;
}
/**
* Method submits local review to the server
* @method submit
* @memberof module:API.cvat.classes.Review
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.DataError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async submit() {
const result = await PluginRegistry.apiWrapper.call(this, Review.prototype.submit);
return result;
}
serialize() {
const { issues, reviewedFrames, reviewedStates } = this;
const data = {
job: this.job,
issue_set: issues.map((issue) => issue.serialize()),
reviewed_frames: Array.from(reviewedFrames),
reviewed_states: Array.from(reviewedStates),
};
if (this.id > 0) {
data.id = this.id;
}
if (typeof this.estimatedQuality !== 'undefined') {
data.estimated_quality = this.estimatedQuality;
}
if (typeof this.status !== 'undefined') {
data.status = this.status;
}
if (this.reviewer) {
data.reviewer = this.reviewer.toJSON();
}
if (this.assignee) {
data.reviewer = this.assignee.toJSON();
}
return data;
}
toJSON() {
const data = this.serialize();
const {
reviewer,
assignee,
reviewed_frames: reviewedFrames,
reviewed_states: reviewedStates,
...updated
} = data;
return {
...updated,
issue_set: this.issues.map((issue) => issue.toJSON()),
reviewer_id: reviewer ? reviewer.id : undefined,
assignee_id: assignee ? assignee.id : undefined,
};
}
async toLocalStorage() {
const data = this.serialize();
store.set(`job-${this.job}-review`, JSON.stringify(data));
}
}
Review.prototype.reviewFrame.implementation = function (frame) {
if (!Number.isInteger(frame)) {
throw new ArgumentError(`The argument "frame" is expected to be an integer. Got ${frame}`);
}
this.__internal.reviewed_frames.add(frame);
};
Review.prototype.reviewStates.implementation = function (stateIDs) {
if (!Array.isArray(stateIDs) || stateIDs.some((stateID) => typeof stateID !== 'string')) {
throw new ArgumentError(`The argument "stateIDs" is expected to be an array of string. Got ${stateIDs}`);
}
stateIDs.forEach((stateID) => this.__internal.reviewed_states.add(stateID));
};
Review.prototype.openIssue.implementation = async function (data) {
if (typeof data !== 'object' || data === null) {
throw new ArgumentError(`The argument "data" must be a not null object. Got ${data}`);
}
if (typeof data.frame !== 'number') {
throw new ArgumentError(`Issue frame must be a number. Got ${data.frame}`);
}
if (!(data.owner instanceof User)) {
throw new ArgumentError(`Issue owner must be a User instance. Got ${data.owner}`);
}
if (!Array.isArray(data.position) || data.position.some((coord) => typeof coord !== 'number')) {
throw new ArgumentError(`Issue position must be an array of numbers. Got ${data.position}`);
}
if (!Array.isArray(data.comment_set)) {
throw new ArgumentError(`Issue comment set must be an array. Got ${data.comment_set}`);
}
const copied = {
frame: data.frame,
position: Issue.hull(data.position),
owner: data.owner,
comment_set: [],
};
const issue = new Issue(copied);
for (const comment of data.comment_set) {
await issue.comment.implementation.call(issue, comment);
}
this.__internal.issue_set.push(issue);
return issue;
};
Review.prototype.submit.implementation = async function () {
if (typeof this.estimatedQuality === 'undefined') {
throw new DataError('Estimated quality is expected to be a number. Got "undefined"');
}
if (typeof this.status === 'undefined') {
throw new DataError('Review status is expected to be a string. Got "undefined"');
}
if (this.id < 0) {
const data = this.toJSON();
const response = await serverProxy.jobs.reviews.create(data);
store.remove(`job-${this.job}-review`);
this.__internal.id = response.id;
this.__internal.issue_set = response.issue_set.map((issue) => new Issue(issue));
this.__internal.estimated_quality = response.estimated_quality;
this.__internal.status = response.status;
if (response.reviewer) this.__internal.reviewer = new User(response.reviewer);
if (response.assignee) this.__internal.assignee = new User(response.assignee);
}
};
module.exports = Review;

@ -1,17 +1,10 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
(() => { (() => {
const FormData = require('form-data'); const FormData = require('form-data');
const { const { ServerError } = require('./exceptions');
ServerError,
} = require('./exceptions');
const store = require('store'); const store = require('store');
const config = require('./config'); const config = require('./config');
const DownloadWorker = require('./download.worker'); const DownloadWorker = require('./download.worker');
@ -38,7 +31,13 @@
if (e.data.isSuccess) { if (e.data.isSuccess) {
requests[e.data.id].resolve(e.data.responseData); requests[e.data.id].resolve(e.data.responseData);
} else { } else {
requests[e.data.id].reject(e.data.error); requests[e.data.id].reject({
error: e.data.error,
response: {
status: e.data.status,
data: e.data.responseData,
},
});
} }
delete requests[e.data.id]; delete requests[e.data.id];
@ -71,12 +70,15 @@
}); });
} }
Object.defineProperties(this, Object.freeze({ Object.defineProperties(
get: { this,
value: get, Object.freeze({
writable: false, get: {
}, value: get,
})); writable: false,
},
}),
);
} }
} }
@ -154,7 +156,6 @@
return response.data; return response.data;
} }
async function userAgreements() { async function userAgreements() {
const { backendAPI } = config; const { backendAPI } = config;
let response = null; let response = null;
@ -169,15 +170,7 @@
return response.data; return response.data;
} }
async function register( async function register(username, firstName, lastName, email, password1, password2, confirmations) {
username,
firstName,
lastName,
email,
password1,
password2,
confirmations,
) {
let response = null; let response = null;
try { try {
const data = JSON.stringify({ const data = JSON.stringify({
@ -203,20 +196,19 @@
} }
async function login(username, password) { async function login(username, password) {
const authenticationData = ([ const authenticationData = [
`${encodeURIComponent('username')}=${encodeURIComponent(username)}`, `${encodeURIComponent('username')}=${encodeURIComponent(username)}`,
`${encodeURIComponent('password')}=${encodeURIComponent(password)}`, `${encodeURIComponent('password')}=${encodeURIComponent(password)}`,
]).join('&').replace(/%20/g, '+'); ]
.join('&')
.replace(/%20/g, '+');
Axios.defaults.headers.common.Authorization = ''; Axios.defaults.headers.common.Authorization = '';
let authenticationResponse = null; let authenticationResponse = null;
try { try {
authenticationResponse = await Axios.post( authenticationResponse = await Axios.post(`${config.backendAPI}/auth/login`, authenticationData, {
`${config.backendAPI}/auth/login`, proxy: config.proxy,
authenticationData, { });
proxy: config.proxy,
},
);
} catch (errorData) { } catch (errorData) {
throw generateError(errorData); throw generateError(errorData);
} }
@ -251,7 +243,7 @@
const data = JSON.stringify({ const data = JSON.stringify({
old_password: oldPassword, old_password: oldPassword,
new_password1: newPassword1, new_password1: newPassword1,
new_password2:newPassword2, new_password2: newPassword2,
}); });
await Axios.post(`${config.backendAPI}/auth/password/change`, data, { await Axios.post(`${config.backendAPI}/auth/password/change`, data, {
proxy: config.proxy, proxy: config.proxy,
@ -264,9 +256,44 @@
} }
} }
async function requestPasswordReset(email) {
try {
const data = JSON.stringify({
email,
});
await Axios.post(`${config.backendAPI}/auth/password/reset`, data, {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData);
}
}
async function resetPassword(newPassword1, newPassword2, uid, _token) {
try {
const data = JSON.stringify({
new_password1: newPassword1,
new_password2: newPassword2,
uid,
token: _token,
});
await Axios.post(`${config.backendAPI}/auth/password/reset/confirm`, data, {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData);
}
}
async function authorized() { async function authorized() {
try { try {
await module.exports.users.getSelf(); await module.exports.users.self();
} catch (serverError) { } catch (serverError) {
if (serverError.code === 401) { if (serverError.code === 401) {
return false; return false;
@ -280,10 +307,88 @@
async function serverRequest(url, data) { async function serverRequest(url, data) {
try { try {
return (await Axios({ return (
url, await Axios({
...data, url,
})).data; ...data,
})
).data;
} catch (errorData) {
throw generateError(errorData);
}
}
async function searchProjectNames(search, limit) {
const { backendAPI, proxy } = config;
let response = null;
try {
response = await Axios.get(
`${backendAPI}/projects?names_only=true&page=1&page_size=${limit}&search=${search}`,
{
proxy,
},
);
} catch (errorData) {
throw generateError(errorData);
}
response.data.results.count = response.data.count;
return response.data.results;
}
async function getProjects(filter = '') {
const { backendAPI, proxy } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/projects?page_size=12&${filter}`, {
proxy,
});
} catch (errorData) {
throw generateError(errorData);
}
response.data.results.count = response.data.count;
return response.data.results;
}
async function saveProject(id, projectData) {
const { backendAPI } = config;
try {
await Axios.patch(`${backendAPI}/projects/${id}`, JSON.stringify(projectData), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData);
}
}
async function deleteProject(id) {
const { backendAPI } = config;
try {
await Axios.delete(`${backendAPI}/projects/${id}`);
} catch (errorData) {
throw generateError(errorData);
}
}
async function createProject(projectSpec) {
const { backendAPI } = config;
try {
const response = await Axios.post(`${backendAPI}/projects`, JSON.stringify(projectSpec), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
return response.data;
} catch (errorData) { } catch (errorData) {
throw generateError(errorData); throw generateError(errorData);
} }
@ -324,7 +429,12 @@
const { backendAPI } = config; const { backendAPI } = config;
try { try {
await Axios.delete(`${backendAPI}/tasks/${id}`); await Axios.delete(`${backendAPI}/tasks/${id}`, {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) { } catch (errorData) {
throw generateError(errorData); throw generateError(errorData);
} }
@ -337,10 +447,9 @@
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
async function request() { async function request() {
try { try {
const response = await Axios const response = await Axios.get(`${url}`, {
.get(`${url}`, { proxy: config.proxy,
proxy: config.proxy, });
});
if (response.status === 202) { if (response.status === 202) {
setTimeout(request, 3000); setTimeout(request, 3000);
} else { } else {
@ -374,21 +483,22 @@
} else if (response.data.state === 'Failed') { } else if (response.data.state === 'Failed') {
// If request has been successful, but task hasn't been created // If request has been successful, but task hasn't been created
// Then passed data is wrong and we can pass code 400 // Then passed data is wrong and we can pass code 400
const message = 'Could not create the task on the server. ' const message = `
+ `${response.data.message}.`; Could not create the task on the server. ${response.data.message}.
`;
reject(new ServerError(message, 400)); reject(new ServerError(message, 400));
} else { } else {
// If server has another status, it is unexpected // If server has another status, it is unexpected
// Therefore it is server error and we can pass code 500 // Therefore it is server error and we can pass code 500
reject(new ServerError( reject(
`Unknown task state has been received: ${response.data.state}`, new ServerError(
500, `Unknown task state has been received: ${response.data.state}`,
)); 500,
),
);
} }
} catch (errorData) { } catch (errorData) {
reject( reject(generateError(errorData));
generateError(errorData),
);
} }
} }
@ -421,7 +531,7 @@
throw generateError(errorData); throw generateError(errorData);
} }
onUpdate('The data is being uploaded to the server..'); onUpdate('The data are being uploaded to the server..');
try { try {
await Axios.post(`${backendAPI}/tasks/${response.data.id}/data`, taskData, { await Axios.post(`${backendAPI}/tasks/${response.data.id}/data`, taskData, {
proxy: config.proxy, proxy: config.proxy,
@ -462,6 +572,90 @@
return response.data; return response.data;
} }
async function getJobReviews(jobID) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/jobs/${jobID}/reviews`, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData);
}
return response.data;
}
async function createReview(data) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.post(`${backendAPI}/reviews`, JSON.stringify(data), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData);
}
return response.data;
}
async function getJobIssues(jobID) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/jobs/${jobID}/issues`, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData);
}
return response.data;
}
async function createComment(data) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.post(`${backendAPI}/comments`, JSON.stringify(data), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData);
}
return response.data;
}
async function updateIssue(issueID, data) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.patch(`${backendAPI}/issues/${issueID}`, JSON.stringify(data), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData);
}
return response.data;
}
async function saveJob(id, jobData) { async function saveJob(id, jobData) {
const { backendAPI } = config; const { backendAPI } = config;
@ -477,20 +671,14 @@
} }
} }
async function getUsers(id = null) { async function getUsers(filter = 'page_size=all') {
const { backendAPI } = config; const { backendAPI } = config;
let response = null; let response = null;
try { try {
if (id === null) { response = await Axios.get(`${backendAPI}/users?${filter}`, {
response = await Axios.get(`${backendAPI}/users?page_size=all`, { proxy: config.proxy,
proxy: config.proxy, });
});
} else {
response = await Axios.get(`${backendAPI}/users/${id}`, {
proxy: config.proxy,
});
}
} catch (errorData) { } catch (errorData) {
throw generateError(errorData); throw generateError(errorData);
} }
@ -524,10 +712,7 @@
}); });
} catch (errorData) { } catch (errorData) {
const code = errorData.response ? errorData.response.status : errorData.code; const code = errorData.response ? errorData.response.status : errorData.code;
throw new ServerError( throw new ServerError(`Could not get preview frame for the task ${tid} from the server`, code);
`Could not get preview frame for the task ${tid} from the server`,
code,
);
} }
return response.data; return response.data;
@ -546,7 +731,14 @@
}, },
); );
} catch (errorData) { } catch (errorData) {
throw generateError(errorData); throw generateError({
...errorData,
message: '',
response: {
...errorData.response,
data: String.fromCharCode.apply(null, new Uint8Array(errorData.response.data)),
},
});
} }
return response; return response;
@ -621,10 +813,13 @@
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
async function request() { async function request() {
try { try {
const response = await Axios const response = await Axios.put(
.put(`${backendAPI}/${session}s/${id}/annotations?format=${format}`, annotationData, { `${backendAPI}/${session}s/${id}/annotations?format=${format}`,
annotationData,
{
proxy: config.proxy, proxy: config.proxy,
}); },
);
if (response.status === 202) { if (response.status === 202) {
annotationData = new FormData(); annotationData = new FormData();
setTimeout(request, 3000); setTimeout(request, 3000);
@ -655,17 +850,19 @@
async function request() { async function request() {
Axios.get(`${url}`, { Axios.get(`${url}`, {
proxy: config.proxy, proxy: config.proxy,
}).then((response) => { })
if (response.status === 202) { .then((response) => {
setTimeout(request, 3000); if (response.status === 202) {
} else { setTimeout(request, 3000);
query = `${query}&action=download`; } else {
url = `${baseURL}?${query}`; query = `${query}&action=download`;
resolve(url); url = `${baseURL}?${query}`;
} resolve(url);
}).catch((errorData) => { }
reject(generateError(errorData)); })
}); .catch((errorData) => {
reject(generateError(errorData));
});
} }
setTimeout(request); setTimeout(request);
@ -704,13 +901,12 @@
const { backendAPI } = config; const { backendAPI } = config;
try { try {
const response = await Axios.post(`${backendAPI}/lambda/requests`, const response = await Axios.post(`${backendAPI}/lambda/requests`, JSON.stringify(body), {
JSON.stringify(body), { proxy: config.proxy,
proxy: config.proxy, headers: {
headers: { 'Content-Type': 'application/json',
'Content-Type': 'application/json', },
}, });
});
return response.data; return response.data;
} catch (errorData) { } catch (errorData) {
@ -722,13 +918,12 @@
const { backendAPI } = config; const { backendAPI } = config;
try { try {
const response = await Axios.post(`${backendAPI}/lambda/functions/${funId}`, const response = await Axios.post(`${backendAPI}/lambda/functions/${funId}`, JSON.stringify(body), {
JSON.stringify(body), { proxy: config.proxy,
proxy: config.proxy, headers: {
headers: { 'Content-Type': 'application/json',
'Content-Type': 'application/json', },
}, });
});
return response.data; return response.data;
} catch (errorData) { } catch (errorData) {
@ -767,99 +962,145 @@
const { backendAPI } = config; const { backendAPI } = config;
try { try {
await Axios.delete( await Axios.delete(`${backendAPI}/lambda/requests/${requestId}`, {
`${backendAPI}/lambda/requests/${requestId}`, { method: 'DELETE',
method: 'DELETE', });
}, } catch (errorData) {
); throw generateError(errorData);
}
}
async function installedApps() {
const { backendAPI } = config;
try {
const response = await Axios.get(`${backendAPI}/server/plugins`, {
proxy: config.proxy,
});
return response.data;
} catch (errorData) { } catch (errorData) {
throw generateError(errorData); throw generateError(errorData);
} }
} }
Object.defineProperties(this, Object.freeze({ Object.defineProperties(
server: { this,
value: Object.freeze({ Object.freeze({
about, server: {
share, value: Object.freeze({
formats, about,
exception, share,
login, formats,
logout, exception,
changePassword, login,
authorized, logout,
register, changePassword,
request: serverRequest, requestPasswordReset,
userAgreements, resetPassword,
}), authorized,
writable: false, register,
}, request: serverRequest,
userAgreements,
tasks: { installedApps,
value: Object.freeze({ }),
getTasks, writable: false,
saveTask, },
createTask,
deleteTask, projects: {
exportDataset, value: Object.freeze({
}), get: getProjects,
writable: false, searchNames: searchProjectNames,
}, save: saveProject,
create: createProject,
jobs: { delete: deleteProject,
value: Object.freeze({ }),
getJob, writable: false,
saveJob, },
}),
writable: false, tasks: {
}, value: Object.freeze({
getTasks,
users: { saveTask,
value: Object.freeze({ createTask,
getUsers, deleteTask,
getSelf, exportDataset,
}), }),
writable: false, writable: false,
}, },
frames: { jobs: {
value: Object.freeze({ value: Object.freeze({
getData, get: getJob,
getMeta, save: saveJob,
getPreview, issues: getJobIssues,
}), reviews: {
writable: false, get: getJobReviews,
}, create: createReview,
},
annotations: { }),
value: Object.freeze({ writable: false,
updateAnnotations, },
getAnnotations,
dumpAnnotations, users: {
uploadAnnotations, value: Object.freeze({
}), get: getUsers,
writable: false, self: getSelf,
}, }),
writable: false,
logs: { },
value: Object.freeze({
save: saveLogs, frames: {
}), value: Object.freeze({
writable: false, getData,
}, getMeta,
getPreview,
lambda: { }),
value: Object.freeze({ writable: false,
list: getLambdaFunctions, },
status: getRequestStatus,
requests: getLambdaRequests, annotations: {
run: runLambdaRequest, value: Object.freeze({
call: callLambdaFunction, updateAnnotations,
cancel: cancelLambdaRequest, getAnnotations,
}), dumpAnnotations,
writable: false, uploadAnnotations,
}, }),
})); writable: false,
},
logs: {
value: Object.freeze({
save: saveLogs,
}),
writable: false,
},
lambda: {
value: Object.freeze({
list: getLambdaFunctions,
status: getRequestStatus,
requests: getLambdaRequests,
run: runLambdaRequest,
call: callLambdaFunction,
cancel: cancelLambdaRequest,
}),
writable: false,
},
issues: {
value: Object.freeze({
update: updateIssue,
}),
writable: false,
},
comments: {
value: Object.freeze({
create: createComment,
}),
writable: false,
},
}),
);
} }
} }

File diff suppressed because it is too large Load Diff

@ -1,97 +1,98 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
(() => { (() => {
/** /**
* Class representing collection statistics * Class representing collection statistics
* @memberof module:API.cvat.classes * @memberof module:API.cvat.classes
* @hideconstructor * @hideconstructor
*/ */
class Statistics { class Statistics {
constructor(label, total) { constructor(label, total) {
Object.defineProperties(this, Object.freeze({ Object.defineProperties(
/** this,
* Statistics by labels with a structure: Object.freeze({
* @example /**
* { * Statistics by labels with a structure:
* label: { * @example
* boxes: { * {
* tracks: 10, * label: {
* shapes: 11, * boxes: {
* }, * tracks: 10,
* polygons: { * shapes: 11,
* tracks: 13, * },
* shapes: 14, * polygons: {
* }, * tracks: 13,
* polylines: { * shapes: 14,
* tracks: 16, * },
* shapes: 17, * polylines: {
* }, * tracks: 16,
* points: { * shapes: 17,
* tracks: 19, * },
* shapes: 20, * points: {
* }, * tracks: 19,
* cuboids: { * shapes: 20,
* tracks: 21, * },
* shapes: 22, * cuboids: {
* }, * tracks: 21,
* tags: 66, * shapes: 22,
* manually: 186, * },
* interpolated: 500, * tags: 66,
* total: 608, * manually: 186,
* } * interpolated: 500,
* } * total: 608,
* @name label * }
* @type {Object} * }
* @memberof module:API.cvat.classes.Statistics * @name label
* @readonly * @type {Object}
* @instance * @memberof module:API.cvat.classes.Statistics
*/ * @readonly
label: { * @instance
get: () => JSON.parse(JSON.stringify(label)), */
}, label: {
/** get: () => JSON.parse(JSON.stringify(label)),
* Total statistics (covers all labels) with a structure: },
* @example /**
* { * Total statistics (covers all labels) with a structure:
* boxes: { * @example
* tracks: 10, * {
* shapes: 11, * boxes: {
* }, * tracks: 10,
* polygons: { * shapes: 11,
* tracks: 13, * },
* shapes: 14, * polygons: {
* }, * tracks: 13,
* polylines: { * shapes: 14,
* tracks: 16, * },
* shapes: 17, * polylines: {
* }, * tracks: 16,
* points: { * shapes: 17,
* tracks: 19, * },
* shapes: 20, * points: {
* }, * tracks: 19,
* cuboids: { * shapes: 20,
* tracks: 21, * },
* shapes: 22, * cuboids: {
* }, * tracks: 21,
* tags: 66, * shapes: 22,
* manually: 186, * },
* interpolated: 500, * tags: 66,
* total: 608, * manually: 186,
* } * interpolated: 500,
* @name total * total: 608,
* @type {Object} * }
* @memberof module:API.cvat.classes.Statistics * @name total
* @readonly * @type {Object}
* @instance * @memberof module:API.cvat.classes.Statistics
*/ * @readonly
total: { * @instance
get: () => JSON.parse(JSON.stringify(total)), */
}, total: {
})); get: () => JSON.parse(JSON.stringify(total)),
},
}),
);
} }
} }

@ -1,14 +1,13 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
(() => { (() => {
/** /**
* Class representing a user * Class representing a user
* @memberof module:API.cvat.classes * @memberof module:API.cvat.classes
* @hideconstructor * @hideconstructor
*/ */
class User { class User {
constructor(initialData) { constructor(initialData) {
const data = { const data = {
@ -27,134 +26,157 @@
}; };
for (const property in data) { for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property) if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
&& property in initialData) {
data[property] = initialData[property]; data[property] = initialData[property];
} }
} }
Object.defineProperties(this, Object.freeze({ Object.defineProperties(
id: { this,
/** Object.freeze({
* @name id id: {
* @type {integer} /**
* @memberof module:API.cvat.classes.User * @name id
* @readonly * @type {integer}
* @instance * @memberof module:API.cvat.classes.User
*/ * @readonly
get: () => data.id, * @instance
}, */
username: { get: () => data.id,
/** },
* @name username username: {
* @type {string} /**
* @memberof module:API.cvat.classes.User * @name username
* @readonly * @type {string}
* @instance * @memberof module:API.cvat.classes.User
*/ * @readonly
get: () => data.username, * @instance
}, */
email: { get: () => data.username,
/** },
* @name email email: {
* @type {string} /**
* @memberof module:API.cvat.classes.User * @name email
* @readonly * @type {string}
* @instance * @memberof module:API.cvat.classes.User
*/ * @readonly
get: () => data.email, * @instance
}, */
firstName: { get: () => data.email,
/** },
* @name firstName firstName: {
* @type {string} /**
* @memberof module:API.cvat.classes.User * @name firstName
* @readonly * @type {string}
* @instance * @memberof module:API.cvat.classes.User
*/ * @readonly
get: () => data.first_name, * @instance
}, */
lastName: { get: () => data.first_name,
/** },
* @name lastName lastName: {
* @type {string} /**
* @memberof module:API.cvat.classes.User * @name lastName
* @readonly * @type {string}
* @instance * @memberof module:API.cvat.classes.User
*/ * @readonly
get: () => data.last_name, * @instance
}, */
groups: { get: () => data.last_name,
/** },
* @name groups groups: {
* @type {string[]} /**
* @memberof module:API.cvat.classes.User * @name groups
* @readonly * @type {string[]}
* @instance * @memberof module:API.cvat.classes.User
*/ * @readonly
get: () => JSON.parse(JSON.stringify(data.groups)), * @instance
}, */
lastLogin: { get: () => JSON.parse(JSON.stringify(data.groups)),
/** },
* @name lastLogin lastLogin: {
* @type {string} /**
* @memberof module:API.cvat.classes.User * @name lastLogin
* @readonly * @type {string}
* @instance * @memberof module:API.cvat.classes.User
*/ * @readonly
get: () => data.last_login, * @instance
}, */
dateJoined: { get: () => data.last_login,
/** },
* @name dateJoined dateJoined: {
* @type {string} /**
* @memberof module:API.cvat.classes.User * @name dateJoined
* @readonly * @type {string}
* @instance * @memberof module:API.cvat.classes.User
*/ * @readonly
get: () => data.date_joined, * @instance
}, */
isStaff: { get: () => data.date_joined,
/** },
* @name isStaff isStaff: {
* @type {boolean} /**
* @memberof module:API.cvat.classes.User * @name isStaff
* @readonly * @type {boolean}
* @instance * @memberof module:API.cvat.classes.User
*/ * @readonly
get: () => data.is_staff, * @instance
}, */
isSuperuser: { get: () => data.is_staff,
/** },
* @name isSuperuser isSuperuser: {
* @type {boolean} /**
* @memberof module:API.cvat.classes.User * @name isSuperuser
* @readonly * @type {boolean}
* @instance * @memberof module:API.cvat.classes.User
*/ * @readonly
get: () => data.is_superuser, * @instance
}, */
isActive: { get: () => data.is_superuser,
/** },
* @name isActive isActive: {
* @type {boolean} /**
* @memberof module:API.cvat.classes.User * @name isActive
* @readonly * @type {boolean}
* @instance * @memberof module:API.cvat.classes.User
*/ * @readonly
get: () => data.is_active, * @instance
}, */
isVerified: { get: () => data.is_active,
/** },
* @name isVerified isVerified: {
* @type {boolean} /**
* @memberof module:API.cvat.classes.User * @name isVerified
* @readonly * @type {boolean}
* @instance * @memberof module:API.cvat.classes.User
*/ * @readonly
get: () => !data.email_verification_required, * @instance
}, */
})); get: () => !data.email_verification_required,
},
}),
);
}
serialize() {
return {
id: this.id,
username: this.username,
email: this.email,
first_name: this.firstName,
last_name: this.lastName,
groups: this.groups,
last_login: this.lastLogin,
date_joined: this.dateJoined,
is_staff: this.isStaff,
is_superuser: this.isSuperuser,
is_active: this.isActive,
email_verification_required: this.isVerified,
};
}
toJSON() {
return this.serialize();
} }
} }

@ -1,13 +1,6 @@
/* // Copyright (C) 2020 Intel Corporation
* Copyright (C) 2018-2020 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
/* global
require:false
jest:false
describe:false
*/
// Setup mock for a server // Setup mock for a server
jest.mock('../../src/server-proxy', () => { jest.mock('../../src/server-proxy', () => {
@ -48,30 +41,25 @@ describe('Feature: get annotations', () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0]; const task = (await window.cvat.tasks.get({ id: 100 }))[0];
// Out of task // Out of task
expect(task.annotations.get(500)) expect(task.annotations.get(500)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
// Out of task // Out of task
expect(task.annotations.get(-1)) expect(task.annotations.get(-1)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
test('get annotations for frame out of job', async () => { test('get annotations for frame out of job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 101 }))[0]; const job = (await window.cvat.jobs.get({ jobID: 101 }))[0];
// Out of segment // Out of segment
expect(job.annotations.get(500)) expect(job.annotations.get(500)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
// Out of segment // Out of segment
expect(job.annotations.get(-1)) expect(job.annotations.get(-1)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
// TODO: Test filter (hasn't been implemented yet) // TODO: Test filter (hasn't been implemented yet)
}); });
describe('Feature: put annotations', () => { describe('Feature: put annotations', () => {
test('put a shape to a task', async () => { test('put a shape to a task', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0]; const task = (await window.cvat.tasks.get({ id: 101 }))[0];
@ -173,8 +161,7 @@ describe('Feature: put annotations', () => {
zOrder: 0, zOrder: 0,
}); });
expect(task.annotations.put([state])) expect(task.annotations.put([state])).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
test('put shape with bad attributes to a task', async () => { test('put shape with bad attributes to a task', async () => {
@ -191,8 +178,7 @@ describe('Feature: put annotations', () => {
zOrder: 0, zOrder: 0,
}); });
expect(task.annotations.put([state])) expect(task.annotations.put([state])).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
test('put shape with bad zOrder to a task', async () => { test('put shape with bad zOrder to a task', async () => {
@ -209,8 +195,7 @@ describe('Feature: put annotations', () => {
zOrder: 'bad value', zOrder: 'bad value',
}); });
expect(task.annotations.put([state])) expect(task.annotations.put([state])).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
const state1 = new window.cvat.classes.ObjectState({ const state1 = new window.cvat.classes.ObjectState({
frame: 1, frame: 1,
@ -223,8 +208,7 @@ describe('Feature: put annotations', () => {
zOrder: NaN, zOrder: NaN,
}); });
expect(task.annotations.put([state1])) expect(task.annotations.put([state1])).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
test('put shape without points and with invalud points to a task', async () => { test('put shape without points and with invalud points to a task', async () => {
@ -240,16 +224,13 @@ describe('Feature: put annotations', () => {
zOrder: 0, zOrder: 0,
}); });
await expect(task.annotations.put([state])) await expect(task.annotations.put([state])).rejects.toThrow(window.cvat.exceptions.DataError);
.rejects.toThrow(window.cvat.exceptions.DataError);
delete state.points; delete state.points;
await expect(task.annotations.put([state])) await expect(task.annotations.put([state])).rejects.toThrow(window.cvat.exceptions.DataError);
.rejects.toThrow(window.cvat.exceptions.DataError);
state.points = ['150,50 250,30']; state.points = ['150,50 250,30'];
expect(task.annotations.put([state])) expect(task.annotations.put([state])).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
test('put shape without type to a task', async () => { test('put shape without type to a task', async () => {
@ -264,8 +245,7 @@ describe('Feature: put annotations', () => {
zOrder: 0, zOrder: 0,
}); });
expect(task.annotations.put([state])) expect(task.annotations.put([state])).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
test('put shape without label and with bad label to a task', async () => { test('put shape without label and with bad label to a task', async () => {
@ -280,16 +260,13 @@ describe('Feature: put annotations', () => {
zOrder: 0, zOrder: 0,
}); });
await expect(task.annotations.put([state])) await expect(task.annotations.put([state])).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
state.label = 'bad label'; state.label = 'bad label';
await expect(task.annotations.put([state])) await expect(task.annotations.put([state])).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
state.label = {}; state.label = {};
await expect(task.annotations.put([state])) await expect(task.annotations.put([state])).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
test('put shape with bad frame to a task', async () => { test('put shape with bad frame to a task', async () => {
@ -305,8 +282,7 @@ describe('Feature: put annotations', () => {
zOrder: 0, zOrder: 0,
}); });
expect(task.annotations.put([state])) expect(task.annotations.put([state])).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
}); });
@ -436,8 +412,7 @@ describe('Feature: save annotations', () => {
// have been sent to a server // have been sent to a server
const oldImplementation = serverProxy.annotations.updateAnnotations; const oldImplementation = serverProxy.annotations.updateAnnotations;
serverProxy.annotations.updateAnnotations = async (session, id, data, action) => { serverProxy.annotations.updateAnnotations = async (session, id, data, action) => {
const result = await oldImplementation const result = await oldImplementation.call(serverProxy.annotations, session, id, data, action);
.call(serverProxy.annotations, session, id, data, action);
if (action === 'delete') { if (action === 'delete') {
okay = okay || (action === 'delete' && !!(data.shapes.length || data.tracks.length)); okay = okay || (action === 'delete' && !!(data.shapes.length || data.tracks.length));
} }
@ -459,10 +434,12 @@ describe('Feature: merge annotations', () => {
const annotations1 = await task.annotations.get(1); const annotations1 = await task.annotations.get(1);
const states = [annotations0[0], annotations1[0]]; const states = [annotations0[0], annotations1[0]];
await task.annotations.merge(states); await task.annotations.merge(states);
const merged0 = (await task.annotations.get(0)) const merged0 = (await task.annotations.get(0)).filter(
.filter((state) => state.objectType === window.cvat.enums.ObjectType.TRACK); (state) => state.objectType === window.cvat.enums.ObjectType.TRACK,
const merged1 = (await task.annotations.get(1)) );
.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(merged0).toHaveLength(1);
expect(merged1).toHaveLength(1); expect(merged1).toHaveLength(1);
@ -476,10 +453,12 @@ describe('Feature: merge annotations', () => {
const annotations1 = await job.annotations.get(1); const annotations1 = await job.annotations.get(1);
const states = [annotations0[0], annotations1[0]]; const states = [annotations0[0], annotations1[0]];
await job.annotations.merge(states); await job.annotations.merge(states);
const merged0 = (await job.annotations.get(0)) const merged0 = (await job.annotations.get(0)).filter(
.filter((state) => state.objectType === window.cvat.enums.ObjectType.TRACK); (state) => state.objectType === window.cvat.enums.ObjectType.TRACK,
const merged1 = (await job.annotations.get(1)) );
.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(merged0).toHaveLength(1);
expect(merged1).toHaveLength(1); expect(merged1).toHaveLength(1);
@ -492,8 +471,7 @@ describe('Feature: merge annotations', () => {
const annotations0 = await task.annotations.get(0); const annotations0 = await task.annotations.get(0);
const states = [annotations0[0], {}]; const states = [annotations0[0], {}];
expect(task.annotations.merge(states)) expect(task.annotations.merge(states)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
test('trying to merge object state which is not saved in a collection', async () => { test('trying to merge object state which is not saved in a collection', async () => {
@ -510,8 +488,7 @@ describe('Feature: merge annotations', () => {
}); });
const states = [annotations0[0], state]; const states = [annotations0[0], state];
expect(task.annotations.merge(states)) expect(task.annotations.merge(states)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
test('trying to merge with bad label', async () => { test('trying to merge with bad label', async () => {
@ -525,19 +502,18 @@ describe('Feature: merge annotations', () => {
attributes: [], attributes: [],
}); });
expect(task.annotations.merge(states)) expect(task.annotations.merge(states)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
test('trying to merge with different shape types', async () => { test('trying to merge with different shape types', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0]; const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations0 = await task.annotations.get(0); const annotations0 = await task.annotations.get(0);
const annotations1 = (await task.annotations.get(1)) const annotations1 = (await task.annotations.get(1)).filter(
.filter((state) => state.shapeType === window.cvat.enums.ObjectShape.POLYGON); (state) => state.shapeType === window.cvat.enums.ObjectShape.POLYGON,
);
const states = [annotations0[0], annotations1[0]]; const states = [annotations0[0], annotations1[0]];
expect(task.annotations.merge(states)) expect(task.annotations.merge(states)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
test('trying to merge with different labels', async () => { test('trying to merge with different labels', async () => {
@ -551,8 +527,7 @@ describe('Feature: merge annotations', () => {
attributes: [], attributes: [],
}); });
expect(task.annotations.merge(states)) expect(task.annotations.merge(states)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
}); });
@ -587,8 +562,9 @@ describe('Feature: split annotations', () => {
const annotations5 = await task.annotations.get(5); const annotations5 = await task.annotations.get(5);
expect(annotations4[0].clientID).toBe(annotations5[0].clientID); expect(annotations4[0].clientID).toBe(annotations5[0].clientID);
expect(task.annotations.split(annotations5[0], 'bad frame')) expect(task.annotations.split(annotations5[0], 'bad frame')).rejects.toThrow(
.rejects.toThrow(window.cvat.exceptions.ArgumentError); window.cvat.exceptions.ArgumentError,
);
}); });
}); });
@ -597,7 +573,7 @@ describe('Feature: group annotations', () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0]; const task = (await window.cvat.tasks.get({ id: 100 }))[0];
let annotations = await task.annotations.get(0); let annotations = await task.annotations.get(0);
const groupID = await task.annotations.group(annotations); const groupID = await task.annotations.group(annotations);
expect(typeof (groupID)).toBe('number'); expect(typeof groupID).toBe('number');
annotations = await task.annotations.get(0); annotations = await task.annotations.get(0);
for (const state of annotations) { for (const state of annotations) {
expect(state.group.id).toBe(groupID); expect(state.group.id).toBe(groupID);
@ -608,7 +584,7 @@ describe('Feature: group annotations', () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0]; const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
let annotations = await job.annotations.get(0); let annotations = await job.annotations.get(0);
const groupID = await job.annotations.group(annotations); const groupID = await job.annotations.group(annotations);
expect(typeof (groupID)).toBe('number'); expect(typeof groupID).toBe('number');
annotations = await job.annotations.get(0); annotations = await job.annotations.get(0);
for (const state of annotations) { for (const state of annotations) {
expect(state.group.id).toBe(groupID); expect(state.group.id).toBe(groupID);
@ -629,15 +605,13 @@ describe('Feature: group annotations', () => {
zOrder: 0, zOrder: 0,
}); });
expect(task.annotations.group([state])) expect(task.annotations.group([state])).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
test('trying to group not object state', async () => { test('trying to group not object state', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0]; const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations = await task.annotations.get(0); const annotations = await task.annotations.get(0);
expect(task.annotations.group(annotations.concat({}))) expect(task.annotations.group(annotations.concat({}))).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
}); });
@ -689,8 +663,7 @@ describe('Feature: clear annotations', () => {
test('clear annotations with bad reload parameter', async () => { test('clear annotations with bad reload parameter', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0]; const task = (await window.cvat.tasks.get({ id: 100 }))[0];
await task.annotations.clear(true); await task.annotations.clear(true);
expect(task.annotations.clear('reload')) expect(task.annotations.clear('reload')).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
}); });
@ -752,18 +725,16 @@ describe('Feature: select object', () => {
test('trying to select from not object states', async () => { test('trying to select from not object states', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0]; const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations = await task.annotations.get(0); const annotations = await task.annotations.get(0);
expect(task.annotations.select(annotations.concat({}), 500, 500)) expect(task.annotations.select(annotations.concat({}), 500, 500)).rejects.toThrow(
.rejects.toThrow(window.cvat.exceptions.ArgumentError); window.cvat.exceptions.ArgumentError,
);
}); });
test('trying to select with invalid coordinates', async () => { test('trying to select with invalid coordinates', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0]; const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations = await task.annotations.get(0); const annotations = await task.annotations.get(0);
expect(task.annotations.select(annotations, null, null)) expect(task.annotations.select(annotations, null, null)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError); expect(task.annotations.select(annotations, null, null)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.select(annotations, null, null)) expect(task.annotations.select(annotations, '5', '10')).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.annotations.select(annotations, '5', '10'))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
}); });

@ -1,13 +1,6 @@
/* // Copyright (C) 2020 Intel Corporation
* Copyright (C) 2018 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
/* global
require:false
jest:false
describe:false
*/
// Setup mock for a server // Setup mock for a server
jest.mock('../../src/server-proxy', () => { jest.mock('../../src/server-proxy', () => {
@ -35,22 +28,18 @@ describe('Feature: get frame meta', () => {
test('pass frame number out of a task', async () => { test('pass frame number out of a task', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0]; const task = (await window.cvat.tasks.get({ id: 100 }))[0];
expect(task.frames.get(100)) expect(task.frames.get(100)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError); expect(task.frames.get(-1)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
expect(task.frames.get(-1))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
test('pass bad frame number', async () => { test('pass bad frame number', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0]; const task = (await window.cvat.tasks.get({ id: 100 }))[0];
expect(task.frames.get('5')) expect(task.frames.get('5')).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
test('do not pass any frame number', async () => { test('do not pass any frame number', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0]; const task = (await window.cvat.tasks.get({ id: 100 }))[0];
expect(task.frames.get()) expect(task.frames.get()).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
}); });
@ -59,14 +48,14 @@ describe('Feature: get frame data', () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0]; const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const frame = await task.frames.get(0); const frame = await task.frames.get(0);
const frameData = await frame.data(); const frameData = await frame.data();
expect(typeof (frameData)).toBe('string'); expect(typeof frameData).toBe('string');
}); });
test('get frame data for a job', async () => { test('get frame data for a job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0]; const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
const frame = await job.frames.get(0); const frame = await job.frames.get(0);
const frameData = await frame.data(); const frameData = await frame.data();
expect(typeof (frameData)).toBe('string'); expect(typeof frameData).toBe('string');
}); });
}); });
@ -74,12 +63,12 @@ describe('Feature: get frame preview', () => {
test('get frame preview for a task', async () => { test('get frame preview for a task', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0]; const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const frame = await task.frames.preview(); const frame = await task.frames.preview();
expect(typeof (frame)).toBe('string'); expect(typeof frame).toBe('string');
}); });
test('get frame preview for a job', async () => { test('get frame preview for a job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0]; const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
const frame = await job.frames.preview(); const frame = await job.frames.preview();
expect(typeof (frame)).toBe('string'); expect(typeof frame).toBe('string');
}); });
}); });

@ -1,13 +1,6 @@
/* // Copyright (C) 2020 Intel Corporation
* Copyright (C) 2018 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
/* global
require:false
jest:false
describe:false
*/
// Setup mock for a server // Setup mock for a server
jest.mock('../../src/server-proxy', () => { jest.mock('../../src/server-proxy', () => {
@ -20,7 +13,6 @@ window.cvat = require('../../src/api');
const { Job } = require('../../src/session'); const { Job } = require('../../src/session');
// Test cases // Test cases
describe('Feature: get a list of jobs', () => { describe('Feature: get a list of jobs', () => {
test('get jobs by a task id', async () => { test('get jobs by a task id', async () => {
@ -45,7 +37,6 @@ describe('Feature: get a list of jobs', () => {
expect(result).toHaveLength(0); expect(result).toHaveLength(0);
}); });
test('get jobs by a job id', async () => { test('get jobs by a job id', async () => {
const result = await window.cvat.jobs.get({ const result = await window.cvat.jobs.get({
jobID: 1, jobID: 1,
@ -64,32 +55,39 @@ describe('Feature: get a list of jobs', () => {
}); });
test('get jobs by invalid filter with both taskID and jobID', async () => { test('get jobs by invalid filter with both taskID and jobID', async () => {
expect(window.cvat.jobs.get({ expect(
taskID: 1, window.cvat.jobs.get({
jobID: 1, taskID: 1,
})).rejects.toThrow(window.cvat.exceptions.ArgumentError); jobID: 1,
}),
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
test('get jobs by invalid job id', async () => { test('get jobs by invalid job id', async () => {
expect(window.cvat.jobs.get({ expect(
jobID: '1', window.cvat.jobs.get({
})).rejects.toThrow(window.cvat.exceptions.ArgumentError); jobID: '1',
}),
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
test('get jobs by invalid task id', async () => { test('get jobs by invalid task id', async () => {
expect(window.cvat.jobs.get({ expect(
taskID: '1', window.cvat.jobs.get({
})).rejects.toThrow(window.cvat.exceptions.ArgumentError); taskID: '1',
}),
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
test('get jobs by unknown filter', async () => { test('get jobs by unknown filter', async () => {
expect(window.cvat.jobs.get({ expect(
unknown: 50, window.cvat.jobs.get({
})).rejects.toThrow(window.cvat.exceptions.ArgumentError); unknown: 50,
}),
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
}); });
describe('Feature: save job', () => { describe('Feature: save job', () => {
test('save status of a job', async () => { test('save status of a job', async () => {
let result = await window.cvat.jobs.get({ let result = await window.cvat.jobs.get({

@ -1,13 +1,6 @@
/* // Copyright (C) 2020 Intel Corporation
* Copyright (C) 2018 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
/* global
require:false
jest:false
describe:false
*/
// Setup mock for a server // Setup mock for a server
jest.mock('../../src/server-proxy', () => { jest.mock('../../src/server-proxy', () => {
@ -168,30 +161,25 @@ describe('Feature: save object from its state', () => {
const state = annotations[0]; const state = annotations[0];
state.occluded = 'false'; state.occluded = 'false';
await expect(state.save()) await expect(state.save()).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
const oldPoints = state.points; const oldPoints = state.points;
state.occluded = false; state.occluded = false;
state.points = ['100', '50', '100', {}]; state.points = ['100', '50', '100', {}];
await expect(state.save()) await expect(state.save()).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
state.points = oldPoints; state.points = oldPoints;
state.lock = 'true'; state.lock = 'true';
await expect(state.save()) await expect(state.save()).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
const oldLabel = state.label; const oldLabel = state.label;
state.lock = false; state.lock = false;
state.label = 1; state.label = 1;
await expect(state.save()) await expect(state.save()).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
state.label = oldLabel; state.label = oldLabel;
state.attributes = { 1: {}, 2: false, 3: () => {} }; state.attributes = { 1: {}, 2: false, 3: () => {} };
await expect(state.save()) await expect(state.save()).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
test('save bad values for a track', async () => { test('save bad values for a track', async () => {
@ -200,40 +188,33 @@ describe('Feature: save object from its state', () => {
const state = annotations[0]; const state = annotations[0];
state.occluded = 'false'; state.occluded = 'false';
await expect(state.save()) await expect(state.save()).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
const oldPoints = state.points; const oldPoints = state.points;
state.occluded = false; state.occluded = false;
state.points = ['100', '50', '100', {}]; state.points = ['100', '50', '100', {}];
await expect(state.save()) await expect(state.save()).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
state.points = oldPoints; state.points = oldPoints;
state.lock = 'true'; state.lock = 'true';
await expect(state.save()) await expect(state.save()).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
const oldLabel = state.label; const oldLabel = state.label;
state.lock = false; state.lock = false;
state.label = 1; state.label = 1;
await expect(state.save()) await expect(state.save()).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
state.label = oldLabel; state.label = oldLabel;
state.outside = 5; state.outside = 5;
await expect(state.save()) await expect(state.save()).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
state.outside = false; state.outside = false;
state.keyframe = '10'; state.keyframe = '10';
await expect(state.save()) await expect(state.save()).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
state.keyframe = true; state.keyframe = true;
state.attributes = { 1: {}, 2: false, 3: () => {} }; state.attributes = { 1: {}, 2: false, 3: () => {} };
await expect(state.save()) await expect(state.save()).rejects.toThrow(window.cvat.exceptions.ArgumentError);
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
test('trying to change locked shape', async () => { test('trying to change locked shape', async () => {

@ -1,13 +1,6 @@
/* // Copyright (C) 2020 Intel Corporation
* Copyright (C) 2018 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
/* global
require:false
jest:false
describe:false
*/
// Setup mock for a server // Setup mock for a server
jest.mock('../../src/server-proxy', () => { jest.mock('../../src/server-proxy', () => {

@ -0,0 +1,170 @@
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
// 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');
const { Project } = require('../../src/project');
describe('Feature: get projects', () => {
test('get all projects', async () => {
const result = await window.cvat.projects.get();
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(2);
for (const el of result) {
expect(el).toBeInstanceOf(Project);
}
});
test('get project by id', async () => {
const result = await window.cvat.projects.get({
id: 2,
});
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(1);
expect(result[0]).toBeInstanceOf(Project);
expect(result[0].id).toBe(2);
expect(result[0].tasks).toHaveLength(1);
expect(result[0].tasks[0]).toBeInstanceOf(Task);
});
test('get a project by an unknown id', async () => {
const result = await window.cvat.projects.get({
id: 1,
});
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(0);
});
test('get a project by an invalid id', async () => {
expect(
window.cvat.projects.get({
id: '1',
}),
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('get projects by filters', async () => {
const result = await window.cvat.projects.get({
status: 'completed',
});
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(1);
expect(result[0]).toBeInstanceOf(Project);
expect(result[0].id).toBe(2);
expect(result[0].status).toBe('completed');
});
test('get projects by invalid filters', async () => {
expect(
window.cvat.projects.get({
unknown: '5',
}),
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
});
describe('Feature: save a project', () => {
test('save some changed fields in a project', async () => {
let result = await window.cvat.tasks.get({
id: 2,
});
result[0].bugTracker = 'newBugTracker';
result[0].name = 'New Project Name';
result[0].save();
result = await window.cvat.tasks.get({
id: 2,
});
expect(result[0].bugTracker).toBe('newBugTracker');
expect(result[0].name).toBe('New Project Name');
});
test('save some new labels in a project', async () => {
let result = await window.cvat.projects.get({
id: 6,
});
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 = [...result[0].labels, newLabel];
result[0].save();
result = await window.cvat.projects.get({
id: 6,
});
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 project without an id', async () => {
const project = new window.cvat.classes.Project({
name: 'New Empty Project',
labels: [
{
name: 'car',
attributes: [
{
default_value: 'false',
input_type: 'checkbox',
mutable: true,
name: 'parked',
values: ['false'],
},
],
},
],
bug_tracker: 'bug tracker value',
});
const result = await project.save();
expect(typeof result.id).toBe('number');
});
});
describe('Feature: delete a project', () => {
test('delete a project', async () => {
let result = await window.cvat.projects.get({
id: 6,
});
await result[0].delete();
result = await window.cvat.projects.get({
id: 6,
});
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(0);
});
});

@ -1,13 +1,6 @@
/* // Copyright (C) 2020 Intel Corporation
* Copyright (C) 2018 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
/* global
require:false
jest:false
describe:false
*/
// Setup mock for a server // Setup mock for a server
jest.mock('../../src/server-proxy', () => { jest.mock('../../src/server-proxy', () => {
@ -17,11 +10,7 @@ jest.mock('../../src/server-proxy', () => {
// Initialize api // Initialize api
window.cvat = require('../../src/api'); window.cvat = require('../../src/api');
const { const { AnnotationFormats, Loader, Dumper } = require('../../src/annotation-formats');
AnnotationFormats,
Loader,
Dumper,
} = require('../../src/annotation-formats');
// Test cases // Test cases
describe('Feature: get info about cvat', () => { describe('Feature: get info about cvat', () => {
@ -34,7 +23,6 @@ describe('Feature: get info about cvat', () => {
}); });
}); });
describe('Feature: get share storage info', () => { describe('Feature: get share storage info', () => {
test('get files in a root of a share storage', async () => { test('get files in a root of a share storage', async () => {
const result = await window.cvat.server.share(); const result = await window.cvat.server.share();
@ -49,9 +37,7 @@ describe('Feature: get share storage info', () => {
}); });
test('get files in a some unknown dir of a share storage', async () => { test('get files in a some unknown dir of a share storage', async () => {
expect(window.cvat.server.share( expect(window.cvat.server.share('Unknown Directory')).rejects.toThrow(window.cvat.exceptions.ServerError);
'Unknown Directory',
)).rejects.toThrow(window.cvat.exceptions.ServerError);
}); });
}); });

@ -1,13 +1,6 @@
/* // Copyright (C) 2020 Intel Corporation
* Copyright (C) 2018 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
/* global
require:false
jest:false
describe:false
*/
// Setup mock for a server // Setup mock for a server
jest.mock('../../src/server-proxy', () => { jest.mock('../../src/server-proxy', () => {
@ -20,7 +13,6 @@ window.cvat = require('../../src/api');
const { Task } = require('../../src/session'); const { Task } = require('../../src/session');
// Test cases // Test cases
describe('Feature: get a list of tasks', () => { describe('Feature: get a list of tasks', () => {
test('get all tasks', async () => { test('get all tasks', async () => {
@ -51,9 +43,11 @@ describe('Feature: get a list of tasks', () => {
}); });
test('get a task by an invalid id', async () => { test('get a task by an invalid id', async () => {
expect(window.cvat.tasks.get({ expect(
id: '50', window.cvat.tasks.get({
})).rejects.toThrow(window.cvat.exceptions.ArgumentError); id: '50',
}),
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
test('get tasks by filters', async () => { test('get tasks by filters', async () => {
@ -69,9 +63,11 @@ describe('Feature: get a list of tasks', () => {
}); });
test('get tasks by invalid filters', async () => { test('get tasks by invalid filters', async () => {
expect(window.cvat.tasks.get({ expect(
unknown: '5', window.cvat.tasks.get({
})).rejects.toThrow(window.cvat.exceptions.ArgumentError); unknown: '5',
}),
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
test('get task by name, status and mode', async () => { test('get task by name, status and mode', async () => {
@ -98,7 +94,6 @@ describe('Feature: save a task', () => {
}); });
result[0].bugTracker = 'newBugTracker'; result[0].bugTracker = 'newBugTracker';
result[0].zOrder = true;
result[0].name = 'New Task Name'; result[0].name = 'New Task Name';
result[0].save(); result[0].save();
@ -108,7 +103,6 @@ describe('Feature: save a task', () => {
}); });
expect(result[0].bugTracker).toBe('newBugTracker'); expect(result[0].bugTracker).toBe('newBugTracker');
expect(result[0].zOrder).toBe(true);
expect(result[0].name).toBe('New Task Name'); expect(result[0].name).toBe('New Task Name');
}); });
@ -119,14 +113,16 @@ describe('Feature: save a task', () => {
const labelsLength = result[0].labels.length; const labelsLength = result[0].labels.length;
const newLabel = new window.cvat.classes.Label({ const newLabel = new window.cvat.classes.Label({
name: 'My boss\'s car', name: "My boss's car",
attributes: [{ attributes: [
default_value: 'false', {
input_type: 'checkbox', default_value: 'false',
mutable: true, input_type: 'checkbox',
name: 'parked', mutable: true,
values: ['false'], name: 'parked',
}], values: ['false'],
},
],
}); });
result[0].labels = [...result[0].labels, newLabel]; result[0].labels = [...result[0].labels, newLabel];
@ -137,7 +133,7 @@ describe('Feature: save a task', () => {
}); });
expect(result[0].labels).toHaveLength(labelsLength + 1); expect(result[0].labels).toHaveLength(labelsLength + 1);
const appendedLabel = result[0].labels.filter((el) => el.name === 'My boss\'s car'); const appendedLabel = result[0].labels.filter((el) => el.name === "My boss's car");
expect(appendedLabel).toHaveLength(1); expect(appendedLabel).toHaveLength(1);
expect(appendedLabel[0].attributes).toHaveLength(1); expect(appendedLabel[0].attributes).toHaveLength(1);
expect(appendedLabel[0].attributes[0].name).toBe('parked'); expect(appendedLabel[0].attributes[0].name).toBe('parked');
@ -149,23 +145,39 @@ describe('Feature: save a task', () => {
test('save new task without an id', async () => { test('save new task without an id', async () => {
const task = new window.cvat.classes.Task({ const task = new window.cvat.classes.Task({
name: 'New Task', name: 'New Task',
labels: [{ labels: [
name: 'My boss\'s car', {
attributes: [{ name: "My boss's car",
default_value: 'false', attributes: [
input_type: 'checkbox', {
mutable: true, default_value: 'false',
name: 'parked', input_type: 'checkbox',
values: ['false'], mutable: true,
}], name: 'parked',
}], values: ['false'],
},
],
},
],
bug_tracker: 'bug tracker value',
image_quality: 50,
});
const result = await task.save();
expect(typeof result.id).toBe('number');
});
test('save new task in project', async () => {
const task = new window.cvat.classes.Task({
name: 'New Task',
project_id: 2,
bug_tracker: 'bug tracker value', bug_tracker: 'bug tracker value',
image_quality: 50, image_quality: 50,
z_order: true, z_order: true,
}); });
const result = await task.save(); const result = await task.save();
expect(typeof (result.id)).toBe('number'); expect(result.projectId).toBe(2);
}); });
}); });

@ -1,13 +1,6 @@
/* // Copyright (C) 2020 Intel Corporation
* Copyright (C) 2018 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
/* global
require:false
jest:false
describe:false
*/
// Setup mock for a server // Setup mock for a server
jest.mock('../../src/server-proxy', () => { jest.mock('../../src/server-proxy', () => {
@ -41,14 +34,18 @@ describe('Feature: get a list of users', () => {
}); });
test('get users with unknown filter key', async () => { test('get users with unknown filter key', async () => {
expect(window.cvat.users.get({ expect(
unknown: '50', window.cvat.users.get({
})).rejects.toThrow(window.cvat.exceptions.ArgumentError); unknown: '50',
}),
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
test('get users with invalid filter key', async () => { test('get users with invalid filter key', async () => {
expect(window.cvat.users.get({ expect(
self: 1, window.cvat.users.get({
})).rejects.toThrow(window.cvat.exceptions.ArgumentError); self: 1,
}),
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
}); });

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save