Merge branch 'release-1.2.0'

main
Nikita Manovich 5 years ago
commit 37d82f9005

@ -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,4 +1,4 @@
exports.settings = {bullet: '*', paddedTable: false} exports.settings = { bullet: '*', paddedTable: false };
exports.plugins = [ exports.plugins = [
'remark-preset-lint-recommended', 'remark-preset-lint-recommended',
@ -7,10 +7,10 @@ exports.plugins = [
['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],
] ];

@ -5,16 +5,18 @@
"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": [
true,
{
"ignoreAtRules": ["extend"] "ignoreAtRules": ["extend"]
}], }
"selector-type-no-unknown": [true, { ],
"selector-type-no-unknown": [
true,
{
"ignoreTypes": ["first-child"] "ignoreTypes": ["first-child"]
}] }
},
"ignoreFiles": [
"**/*.js",
"**/*.ts",
"**/*.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,6 +7,8 @@ cache:
- ~/.cache - ~/.cache
addons: addons:
firefox: 'latest'
chrome: stable
apt: apt:
packages: packages:
- libgconf-2-4 - libgconf-2-4
@ -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>)
@ -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,33 +10,33 @@ 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 ```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 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
``` ```
Also please make sure that you have installed ffmpeg with all necessary libav* libraries and pkg-config package.
```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) # 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 - curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt-get install -y nodejs sudo apt-get install -y nodejs
```
# General dependencies MacOS 10.15
sudo apt-get install -y pkg-config
# Library components ```sh
sudo apt-get install -y \ brew install git python pyenv redis curl openssl node
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 FFmpeg libraries (libav\*) version 4.0 or higher.
- Install [Visual Studio Code](https://code.visualstudio.com/docs/setup/linux#_debian-and-ubuntu-based-distributions) - Install [Visual Studio Code](https://code.visualstudio.com/docs/setup/linux#_debian-and-ubuntu-based-distributions)
for development for development
- Install CVAT on your local host: - Install CVAT on your local host:
```sh ```sh
git clone https://github.com/opencv/cvat git clone https://github.com/opencv/cvat
cd cvat && mkdir logs keys cd cvat && mkdir logs keys
@ -44,12 +44,18 @@ for development
. .env/bin/activate . .env/bin/activate
pip install -U pip wheel setuptools pip install -U pip wheel setuptools
pip install -r cvat/requirements/development.txt pip install -r cvat/requirements/development.txt
pip install -r datumaro/requirements.txt
python manage.py migrate python manage.py migrate
python manage.py collectstatic 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: - Create a super user for CVAT:
```sh ```sh
$ python manage.py createsuperuser $ python manage.py createsuperuser
Username (leave blank to use 'django'): *** Username (leave blank to use 'django'): ***
@ -59,18 +65,29 @@ for development
``` ```
- Install npm packages for UI and start UI debug server (run the following command from CVAT root directory): - Install npm packages for UI and start UI debug server (run the following command from CVAT root directory):
```sh ```sh
npm install && \ npm ci && \
cd cvat-core && npm install && \ cd cvat-core && npm ci && \
cd ../cvat-ui && npm install && npm start cd ../cvat-ui && npm ci && npm start
``` ```
> 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 - Open new terminal (Ctrl + Shift + T), run Visual Studio Code from the virtual environment
```sh ```sh
cd .. && source .env/bin/activate && code cd .. && source .env/bin/activate && code
``` ```
- Install following VS Code extensions: - Install following VS Code extensions:
- [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome) - [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome)
- [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python) - [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python)
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
@ -94,19 +111,21 @@ You develop CVAT under WSL (Windows subsystem for Linux) following next steps.
- 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 ```powershell
wsl -d Ubuntu-18.04 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)
@ -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:
@ -272,6 +292,7 @@ requests](#pull-requests), but please respect the following restrictions:
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.
@ -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

@ -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
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 python3 -m pip install --no-cache-dir -r ${HOME}/datumaro/requirements.txt # 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
RUN chown -R ${USER}:${USER} . # 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,16 +40,19 @@ 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.
For more information about supported formats look at the
[documentation](cvat/apps/dataset_manager/formats/README.md#formats).
| Annotation format | Import | Export | | Annotation format | Import | Export |
| ------------------------------------------------------------------------------------------ | ------ | ------ | | ----------------------------------------------------------------------------- | ------ | ------ |
| [CVAT for images](cvat/apps/documentation/xml_format.md#annotation) | X | X | | [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 | | [CVAT for a video](cvat/apps/documentation/xml_format.md#interpolation) | X | X |
| [Datumaro](datumaro/README.md) | | X | | [Datumaro](https://github.com/openvinotoolkit/datumaro) | | X |
| [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X | | [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X |
| Segmentation masks from [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X | | Segmentation masks from [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) | X | X |
| [YOLO](https://pjreddie.com/darknet/yolo/) | X | X | | [YOLO](https://pjreddie.com/darknet/yolo/) | X | X |
@ -57,19 +60,21 @@ via its command line tool and Python library.
| [TFrecord](https://www.tensorflow.org/tutorials/load_data/tf_records) | X | X | | [TFrecord](https://www.tensorflow.org/tutorials/load_data/tf_records) | X | X |
| [MOT](https://motchallenge.net/) | X | X | | [MOT](https://motchallenge.net/) | X | X |
| [LabelMe 3.0](http://labelme.csail.mit.edu/Release3.0) | 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,6 +21,7 @@ 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",

@ -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}
@ -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,11 +185,14 @@ 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();
@ -185,27 +214,33 @@ Standard JS events are used.
}); });
``` ```
<!--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<
number,
Record<
number,
{
click: (event: MouseEvent) => void; click: (event: MouseEvent) => void;
dblclick: (event: MouseEvent) => void; dblclick: (event: MouseEvent) => void;
}>>; }
>
>;
public constructor(frameContent: SVGSVGElement) { public constructor(frameContent: SVGSVGElement) {
this.frameContent = frameContent; this.frameContent = frameContent;
@ -47,8 +53,7 @@ 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();
@ -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,15 +109,14 @@ 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');
@ -127,9 +132,7 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
event.stopPropagation(); event.stopPropagation();
// another shape was clicked // another shape was clicked
if (this.auxiliaryGroupID !== null if (this.auxiliaryGroupID !== null && this.auxiliaryGroupID !== groupID) {
&& this.auxiliaryGroupID !== groupID
) {
this.resetAuxiliaryShape(); this.resetAuxiliaryShape();
} }
@ -169,9 +172,10 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
} else { } else {
// sign defines bypass direction // sign defines bypass direction
const landmarks = this.auxiliaryClicks; const landmarks = this.auxiliaryClicks;
const sign = Math.sign(landmarks[2] - landmarks[0]) const sign =
* Math.sign(landmarks[1] - landmarks[0]) Math.sign(landmarks[2] - landmarks[0]) *
* Math.sign(landmarks[2] - landmarks[1]); Math.sign(landmarks[1] - landmarks[0]) *
Math.sign(landmarks[2] - landmarks[1]);
// go via a polygon and get vertexes // go via a polygon and get vertexes
// the first vertex has been already drawn // the first vertex has been already drawn
@ -195,7 +199,8 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
// remove the latest cursor position from drawing array // remove the latest cursor position from drawing array
for (const wayPoint of way) { for (const wayPoint of way) {
const [_x, _y] = wayPoint.split(',') const [_x, _y] = wayPoint
.split(',')
.map((coordinate: string): number => +coordinate); .map((coordinate: string): number => +coordinate);
this.addPointToCurrentShape(_x, _y); this.addPointToCurrentShape(_x, _y);
} }
@ -204,7 +209,6 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
} }
}; };
const dblclick = (event: MouseEvent): void => { const dblclick = (event: MouseEvent): void => {
event.stopPropagation(); event.stopPropagation();
}; };
@ -217,11 +221,13 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
circle.addEventListener('mousedown', this.listeners[groupID][pointID].click); circle.addEventListener('mousedown', this.listeners[groupID][pointID].click);
circle.addEventListener('dblclick', this.listeners[groupID][pointID].click); circle.addEventListener('dblclick', this.listeners[groupID][pointID].click);
return circle; return circle;
}); },
);
group.append(...circles); group.append(...circles);
return group; return group;
}); },
);
this.frameContent.append(...this.groups); this.frameContent.append(...this.groups);
} }
@ -231,9 +237,11 @@ 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 transformedShapes = shapes
.map((shape: HTMLElement): TransformedShape | null => {
const color = shape.getAttribute('fill'); const color = shape.getAttribute('fill');
const clientID = shape.getAttribute('clientID'); const clientID = shape.getAttribute('clientID');
@ -270,16 +278,13 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
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,20 +392,20 @@ 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;
@ -366,15 +413,21 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
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 => { })
.catch((exception: any): void => {
this.data.exception = exception;
this.notify(UpdateReasons.DATA_FAILED);
throw exception; 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 {
@ -347,8 +348,8 @@ 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);
@ -460,7 +461,6 @@ 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) {
@ -470,18 +470,12 @@ export function cuboidFrom4Points(flattenedPoints: any[]): any[] {
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,7 +247,8 @@ 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
.on('drawstop', (e: Event): void => {
const bbox = (e.target as SVGRectElement).getBBox(); const bbox = (e.target as SVGRectElement).getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox); const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
const { shapeType, redraw: clientID } = this.drawData; const { shapeType, redraw: clientID } = this.drawData;
@ -269,29 +256,39 @@ export class DrawHandlerImpl implements DrawHandler {
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, clientID,
shapeType, shapeType,
points: [xtl, ytl, xbr, ybr], points: [xtl, ytl, xbr, ybr],
}, Date.now() - this.startTimestamp); },
Date.now() - this.startTimestamp,
);
} }
}).on('drawupdate', (): void => { })
.on('drawupdate', (): void => {
this.shapeSizeElement.update(this.drawInstance); this.shapeSizeElement.update(this.drawInstance);
}).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,
}); });
} }
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, shapeType,
clientID, clientID,
points: [xtl, ytl, xbr, ybr], points: [xtl, ytl, xbr, ybr],
}, Date.now() - this.startTimestamp); },
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 } =
shapeType === 'cuboid'
? this.getFinalCuboidCoordinates(targetPoints)
: this.getFinalPolyshapeCoordinates(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
) {
this.onDrawDone(
{
clientID, clientID,
shapeType, shapeType,
points, points,
}, Date.now() - this.startTimestamp); },
} else if (shapeType === 'polyline' Date.now() - this.startTimestamp,
&& ((box.xbr - box.xtl) >= consts.SIZE_THRESHOLD );
|| (box.ybr - box.ytl) >= consts.SIZE_THRESHOLD) } else if (
&& points.length >= 2 * 2) { shapeType === 'polyline' &&
this.onDrawDone({ (box.xbr - box.xtl >= consts.SIZE_THRESHOLD || box.ybr - box.ytl >= consts.SIZE_THRESHOLD) &&
points.length >= 2 * 2
) {
this.onDrawDone(
{
clientID, clientID,
shapeType, shapeType,
points, points,
}, Date.now() - this.startTimestamp); },
} else if (shapeType === 'points' Date.now() - this.startTimestamp,
&& (e.target as any).getAttribute('points') !== '0,0') { );
this.onDrawDone({ } else if (shapeType === 'points' && (e.target as any).getAttribute('points') !== '0,0') {
this.onDrawDone(
{
clientID, clientID,
shapeType, shapeType,
points, points,
}, Date.now() - this.startTimestamp); },
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,8 +472,7 @@ 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,
}); });
@ -461,8 +481,10 @@ export class DrawHandlerImpl implements DrawHandler {
} }
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,7 +492,8 @@ 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
.on('drawstop', (e: Event): void => {
const bbox = (e.target as SVGRectElement).getBBox(); const bbox = (e.target as SVGRectElement).getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox); const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
const { shapeType } = this.drawData; const { shapeType } = this.drawData;
@ -479,14 +502,20 @@ export class DrawHandlerImpl implements DrawHandler {
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, shapeType,
points: cuboidFrom4Points([xtl, ybr, xbr, ybr, xbr, ytl, xbr + d.x, ytl - d.y]), points: cuboidFrom4Points([xtl, ybr, xbr, ybr, xbr, ytl, xbr + d.x, ytl - d.y]),
}, Date.now() - this.startTimestamp); },
Date.now() - this.startTimestamp,
);
} }
}).on('drawupdate', (): void => { })
.on('drawupdate', (): void => {
this.shapeSizeElement.update(this.drawInstance); this.shapeSizeElement.update(this.drawInstance);
}).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,
}); });
} }
@ -498,14 +527,17 @@ 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.drawData.initialState.shapeType === 'cuboid'
? this.getFinalCuboidCoordinates(targetPoints)
: this.getFinalPolyshapeCoordinates(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, shapeType: this.drawData.initialState.shapeType,
objectType: this.drawData.initialState.objectType, objectType: this.drawData.initialState.objectType,
points, points,
@ -513,7 +545,10 @@ export class DrawHandlerImpl implements DrawHandler {
attributes: { ...this.drawData.initialState.attributes }, attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label, label: this.drawData.initialState.label,
color: this.drawData.initialState.color, color: this.drawData.initialState.color,
}, Date.now() - this.startTimestamp, e.detail.originalEvent.ctrlKey); },
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,7 +585,8 @@ export class DrawHandlerImpl implements DrawHandler {
this.release(); this.release();
} }
this.onDrawDone({ this.onDrawDone(
{
shapeType: this.drawData.initialState.shapeType, shapeType: this.drawData.initialState.shapeType,
objectType: this.drawData.initialState.objectType, objectType: this.drawData.initialState.objectType,
points: [xtl, ytl, xbr, ybr], points: [xtl, ytl, xbr, ybr],
@ -556,14 +594,18 @@ export class DrawHandlerImpl implements DrawHandler {
attributes: { ...this.drawData.initialState.attributes }, attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label, label: this.drawData.initialState.label,
color: this.drawData.initialState.color, color: this.drawData.initialState.color,
}, Date.now() - this.startTimestamp, e.detail.originalEvent.ctrlKey); },
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,7 +624,10 @@ 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)
.cube(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,
'face-stroke': 'black', 'face-stroke': 'black',
}); });
@ -589,13 +636,7 @@ export class DrawHandlerImpl implements DrawHandler {
} }
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,8 +651,7 @@ 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,
}); });
@ -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)
.addClass('cvat_canvas_shape_drawing')
.style({
'pointer-events': 'none', 'pointer-events': 'none',
'fill-opacity': 0, 'fill-opacity': 0,
'stroke': strokeColor, stroke: strokeColor,
}).attr({ })
.attr({
'data-origin-client-id': this.editData.state.clientID, 'data-origin-client-id': this.editData.state.clientID,
}).on('drawstart drawpoint', (e: CustomEvent): void => { })
.on('drawstart drawpoint', (e: CustomEvent): void => {
this.transform(this.geometry); this.transform(this.geometry);
lastDrawnPoint.x = e.detail.event.clientX; lastDrawnPoint.x = e.detail.event.clientX;
lastDrawnPoint.y = e.detail.event.clientY; lastDrawnPoint.y = e.detail.event.clientY;
}).draw(dummyEvent, { snapToGrid: 0.1 }); })
.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(
this.canvas
.polygon(points.join(' '))
.attr('fill', this.editedShape.attr('fill')) .attr('fill', this.editedShape.attr('fill'))
.attr('fill-opacity', '0.5') .attr('fill-opacity', '0.5')
.addClass('cvat_canvas_shape')); .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
.on('mouseenter', (): void => {
clone.addClass('cvat_canvas_shape_splitting'); clone.addClass('cvat_canvas_shape_splitting');
}).on('mouseleave', (): void => { })
.on('mouseleave', (): void => {
clone.removeClass('cvat_canvas_shape_splitting'); 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(
event: MouseEvent,
): {
xtl: number; xtl: number;
ytl: number; ytl: number;
xbr: number; xbr: number;
ybr: number; ybr: 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],
@ -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
.text('')
.font({
weight: 'bolder', weight: 'bolder',
}).fill('white').addClass('cvat_canvas_text'), })
.fill('white')
.addClass('cvat_canvas_text'),
update(shape: SVG.Shape): void { 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
.trim()
.split(/\s/)
.map(
(point: string): Point => {
const [x, y] = point.split(',').map((coord: string): number => +coord); const [x, y] = point.split(',').map((coord: string): number => +coord);
return { x, y }; 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(
'click.split',
(): void => {
this.splitDone = true; this.splitDone = true;
this.onSplitDone(state); this.onSplitDone(state);
}, { },
{
once: true, once: true,
}); },
);
} }
} }
} }

@ -10,13 +10,7 @@ import 'svg.select.js';
import 'svg.draw.js'; import 'svg.draw.js';
import consts from './consts'; import consts from './consts';
import { import { Point, Equation, CuboidModel, Orientation, Edge } from './cuboid';
Point,
Equation,
CuboidModel,
Orientation,
Edge,
} from './cuboid';
import { parsePoints, clamp } from './shared'; import { parsePoints, clamp } from './shared';
// Update constructor // Update constructor
@ -51,20 +45,19 @@ function undo(): void {
} }
} }
SVG.Element.prototype.draw.extend('polyline', Object.assign({}, SVG.Element.prototype.draw.extend(
SVG.Element.prototype.draw.plugins.polyline, 'polyline',
{ Object.assign({}, SVG.Element.prototype.draw.plugins.polyline, {
undo: undo, undo: undo,
}, }),
)); );
SVG.Element.prototype.draw.extend('polygon', Object.assign({}, SVG.Element.prototype.draw.extend(
SVG.Element.prototype.draw.plugins.polygon, 'polygon',
{ Object.assign({}, SVG.Element.prototype.draw.plugins.polygon, {
undo: undo, undo: undo,
}, }),
)); );
// Create transform for rect, polyline and polygon // Create transform for rect, polyline and polygon
function transform(): void { function transform(): void {
@ -72,26 +65,26 @@ function transform(): void {
this.offset = { x: window.pageXOffset, y: window.pageYOffset }; this.offset = { x: window.pageXOffset, y: window.pageYOffset };
} }
SVG.Element.prototype.draw.extend('rect', Object.assign({}, SVG.Element.prototype.draw.extend(
SVG.Element.prototype.draw.plugins.rect, 'rect',
{ Object.assign({}, SVG.Element.prototype.draw.plugins.rect, {
transform: transform, transform: transform,
}, }),
)); );
SVG.Element.prototype.draw.extend('polyline', Object.assign({}, SVG.Element.prototype.draw.extend(
SVG.Element.prototype.draw.plugins.polyline, 'polyline',
{ Object.assign({}, SVG.Element.prototype.draw.plugins.polyline, {
transform: transform, transform: transform,
}, }),
)); );
SVG.Element.prototype.draw.extend('polygon', Object.assign({}, SVG.Element.prototype.draw.extend(
SVG.Element.prototype.draw.plugins.polygon, 'polygon',
{ Object.assign({}, SVG.Element.prototype.draw.plugins.polygon, {
transform: transform, transform: transform,
}, }),
)); );
// Fix method drawCircles // Fix method drawCircles
function drawCircles(): void { function drawCircles(): void {
@ -108,9 +101,7 @@ function drawCircles(): void {
[, this.p.y] = array[i]; [, this.p.y] = array[i];
const p = this.p.matrixTransform( const p = this.p.matrixTransform(
this.parent.node.getScreenCTM() this.parent.node.getScreenCTM().inverse().multiply(this.el.node.getScreenCTM()),
.inverse()
.multiply(this.el.node.getScreenCTM()),
); );
this.set.add( this.set.add(
@ -118,32 +109,33 @@ function drawCircles(): void {
.circle(5) .circle(5)
.stroke({ .stroke({
width: 1, width: 1,
}).fill('#ccc') })
.fill('#ccc')
.center(p.x, p.y), .center(p.x, p.y),
); );
} }
} }
SVG.Element.prototype.draw.extend('line', Object.assign({}, SVG.Element.prototype.draw.extend(
SVG.Element.prototype.draw.plugins.line, 'line',
{ Object.assign({}, SVG.Element.prototype.draw.plugins.line, {
drawCircles: drawCircles, drawCircles: drawCircles,
} }),
)); );
SVG.Element.prototype.draw.extend('polyline', Object.assign({}, SVG.Element.prototype.draw.extend(
SVG.Element.prototype.draw.plugins.polyline, 'polyline',
{ Object.assign({}, SVG.Element.prototype.draw.plugins.polyline, {
drawCircles: drawCircles, drawCircles: drawCircles,
} }),
)); );
SVG.Element.prototype.draw.extend('polygon', Object.assign({}, SVG.Element.prototype.draw.extend(
SVG.Element.prototype.draw.plugins.polygon, 'polygon',
{ Object.assign({}, SVG.Element.prototype.draw.plugins.polygon, {
drawCircles: drawCircles, drawCircles: drawCircles,
} }),
)); );
// Fix method drag // Fix method drag
const originalDraggable = SVG.Element.prototype.draggable; const originalDraggable = SVG.Element.prototype.draggable;
@ -155,7 +147,7 @@ SVG.Element.prototype.draggable = function constructor(...args: any): any {
handler.drag = function (e: any) { handler.drag = function (e: any) {
this.m = this.el.node.getScreenCTM().inverse(); this.m = this.el.node.getScreenCTM().inverse();
return handler.constructor.prototype.drag.call(this, e); return handler.constructor.prototype.drag.call(this, e);
} };
} else { } else {
originalDraggable.call(this, ...args); originalDraggable.call(this, ...args);
} }
@ -175,14 +167,14 @@ SVG.Element.prototype.resize = function constructor(...args: any): any {
handler = this.remember('_resizeHandler'); handler = this.remember('_resizeHandler');
handler.resize = function (e: any) { handler.resize = function (e: any) {
const { event } = e.detail; const { event } = e.detail;
if (event.button === 0 && !event.shiftKey && !event.ctrlKey) { if (event.button === 0 && !event.shiftKey && !event.altKey) {
return handler.constructor.prototype.resize.call(this, e); return handler.constructor.prototype.resize.call(this, e);
} }
} };
handler.update = function (e: any) { handler.update = function (e: any) {
this.m = this.el.node.getScreenCTM().inverse(); this.m = this.el.node.getScreenCTM().inverse();
return handler.constructor.prototype.update.call(this, e); return handler.constructor.prototype.update.call(this, e);
} };
} else { } else {
originalResize.call(this, ...args); originalResize.call(this, ...args);
} }
@ -193,7 +185,6 @@ for (const key of Object.keys(originalResize)) {
SVG.Element.prototype.resize[key] = originalResize[key]; SVG.Element.prototype.resize[key] = originalResize[key];
} }
enum EdgeIndex { enum EdgeIndex {
FL = 1, FL = 1,
FR = 2, FR = 2,
@ -255,14 +246,34 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
}, },
setupProjections() { setupProjections() {
this.ftProj = this.line(this.updateProjectionLine(this.cuboidModel.ft.getEquation(), this.ftProj = this.line(
this.cuboidModel.ft.points[0], this.cuboidModel.vpl)); this.updateProjectionLine(
this.fbProj = this.line(this.updateProjectionLine(this.cuboidModel.fb.getEquation(), this.cuboidModel.ft.getEquation(),
this.cuboidModel.ft.points[0], this.cuboidModel.vpl)); this.cuboidModel.ft.points[0],
this.rtProj = this.line(this.updateProjectionLine(this.cuboidModel.rt.getEquation(), this.cuboidModel.vpl,
this.cuboidModel.rt.points[1], this.cuboidModel.vpr)); ),
this.rbProj = this.line(this.updateProjectionLine(this.cuboidModel.rb.getEquation(), );
this.cuboidModel.rb.points[1], this.cuboidModel.vpr)); this.fbProj = this.line(
this.updateProjectionLine(
this.cuboidModel.fb.getEquation(),
this.cuboidModel.ft.points[0],
this.cuboidModel.vpl,
),
);
this.rtProj = this.line(
this.updateProjectionLine(
this.cuboidModel.rt.getEquation(),
this.cuboidModel.rt.points[1],
this.cuboidModel.vpr,
),
);
this.rbProj = this.line(
this.updateProjectionLine(
this.cuboidModel.rb.getEquation(),
this.cuboidModel.rb.points[1],
this.cuboidModel.vpr,
),
);
this.ftProj.stroke({ color: '#C0C0C0' }).addClass('cvat_canvas_cuboid_projections'); this.ftProj.stroke({ color: '#C0C0C0' }).addClass('cvat_canvas_cuboid_projections');
this.fbProj.stroke({ color: '#C0C0C0' }).addClass('cvat_canvas_cuboid_projections'); this.fbProj.stroke({ color: '#C0C0C0' }).addClass('cvat_canvas_cuboid_projections');
@ -308,8 +319,6 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
} else { } else {
this.drCenter.hide(); this.drCenter.hide();
} }
}, },
showProjections() { showProjections() {
@ -358,7 +367,10 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
const x2 = direction.x; const x2 = direction.x;
const y2 = equation.getY(x2); const y2 = equation.getY(x2);
return [[x1, y1], [x2, y2]]; return [
[x1, y1],
[x2, y2],
];
}, },
selectize(value: boolean, options: object) { selectize(value: boolean, options: object) {
@ -373,40 +385,46 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
} }
if (value === false) { if (value === false) {
this.getGrabPoints().forEach((point: SVG.Element) => {point && point.remove()}); this.getGrabPoints().forEach((point: SVG.Element) => {
point && point.remove();
});
} else { } else {
this.setupGrabPoints(this.face.remember('_selectHandler').drawPoint.bind( this.setupGrabPoints(
{nested: this, options: this.face.remember('_selectHandler').options} this.face
)); .remember('_selectHandler')
.drawPoint.bind({ nested: this, options: this.face.remember('_selectHandler').options }),
);
// setup proper classes for selection points for proper cursor // setup proper classes for selection points for proper cursor
Array.from(this.face.remember('_selectHandler').nested.node.children) Array.from(this.face.remember('_selectHandler').nested.node.children).forEach(
.forEach((point: SVG.LinkedHTMLElement, i: number) => { (point: SVG.LinkedHTMLElement, i: number) => {
point.classList.add(`svg_select_points_${['lt', 'lb', 'rb', 'rt'][i]}`) point.classList.add(`svg_select_points_${['lt', 'lb', 'rb', 'rt'][i]}`);
}); },
);
if (this.cuboidModel.orientation === Orientation.LEFT) { if (this.cuboidModel.orientation === Orientation.LEFT) {
Array.from(this.dorsalRightEdge.remember('_selectHandler').nested.node.children) Array.from(this.dorsalRightEdge.remember('_selectHandler').nested.node.children).forEach(
.forEach((point: SVG.LinkedHTMLElement, i: number) => { (point: SVG.LinkedHTMLElement, i: number) => {
point.classList.add(`svg_select_points_${['t', 'b'][i]}`); point.classList.add(`svg_select_points_${['t', 'b'][i]}`);
point.ondblclick = (e: MouseEvent) => { point.ondblclick = (e: MouseEvent) => {
if (e.shiftKey) { if (e.shiftKey) {
this.resetPerspective() this.resetPerspective();
} }
}; };
}); },
);
} else { } else {
Array.from(this.dorsalLeftEdge.remember('_selectHandler').nested.node.children) Array.from(this.dorsalLeftEdge.remember('_selectHandler').nested.node.children).forEach(
.forEach((point: SVG.LinkedHTMLElement, i: number) => { (point: SVG.LinkedHTMLElement, i: number) => {
point.classList.add(`svg_select_points_${['t', 'b'][i]}`); point.classList.add(`svg_select_points_${['t', 'b'][i]}`);
point.ondblclick = (e: MouseEvent) => { point.ondblclick = (e: MouseEvent) => {
if (e.shiftKey) { if (e.shiftKey) {
this.resetPerspective() this.resetPerspective();
} }
}; };
}); },
);
} }
} }
return this; return this;
@ -428,7 +446,7 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
point.off('dragmove'); point.off('dragmove');
point.off('dragend'); point.off('dragend');
} }
}) });
return; return;
} }
@ -436,9 +454,7 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
function getResizedPointIndex(event: CustomEvent): number { function getResizedPointIndex(event: CustomEvent): number {
const { target } = event.detail.event.detail.event; const { target } = event.detail.event.detail.event;
const { parentElement } = target; const { parentElement } = target;
return Array return Array.from(parentElement.children).indexOf(target);
.from(parentElement.children)
.indexOf(target);
} }
let resizedCubePoint: null | number = null; let resizedCubePoint: null | number = null;
@ -447,14 +463,15 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
y: 0, y: 0,
}; };
this.face.on('resizestart', (event: CustomEvent) => { this.face
.on('resizestart', (event: CustomEvent) => {
accumulatedOffset.x = 0; accumulatedOffset.x = 0;
accumulatedOffset.y = 0; accumulatedOffset.y = 0;
const resizedFacePoint = getResizedPointIndex(event); const resizedFacePoint = getResizedPointIndex(event);
resizedCubePoint = [0, 1].includes(resizedFacePoint) ? resizedFacePoint resizedCubePoint = [0, 1].includes(resizedFacePoint) ? resizedFacePoint : 5 - resizedFacePoint; // 2,3 -> 3,2
: 5 - resizedFacePoint; // 2,3 -> 3,2
this.fire(new CustomEvent('resizestart', event)); this.fire(new CustomEvent('resizestart', event));
}).on('resizing', (event: CustomEvent) => { })
.on('resizing', (event: CustomEvent) => {
let { dx, dy } = event.detail; let { dx, dy } = event.detail;
let dxPortion = dx - accumulatedOffset.x; let dxPortion = dx - accumulatedOffset.x;
let dyPortion = dy - accumulatedOffset.y; let dyPortion = dy - accumulatedOffset.y;
@ -467,13 +484,15 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
let cuboidPoints = this.cuboidModel.getPoints(); let cuboidPoints = this.cuboidModel.getPoints();
let x1 = cuboidPoints[edgeTopIndex].x + dxPortion; let x1 = cuboidPoints[edgeTopIndex].x + dxPortion;
let x2 = cuboidPoints[edgeBottomIndex].x + dxPortion; let x2 = cuboidPoints[edgeBottomIndex].x + dxPortion;
if (edge === EdgeIndex.FL if (
&& (cuboidPoints[2].x - (cuboidPoints[0].x + dxPortion) < consts.MIN_EDGE_LENGTH) edge === EdgeIndex.FL &&
cuboidPoints[2].x - (cuboidPoints[0].x + dxPortion) < consts.MIN_EDGE_LENGTH
) { ) {
x1 = cuboidPoints[edgeTopIndex].x; x1 = cuboidPoints[edgeTopIndex].x;
x2 = cuboidPoints[edgeBottomIndex].x; x2 = cuboidPoints[edgeBottomIndex].x;
} else if (edge === EdgeIndex.FR } else if (
&& (cuboidPoints[2].x + dxPortion - cuboidPoints[0].x < consts.MIN_EDGE_LENGTH) edge === EdgeIndex.FR &&
cuboidPoints[2].x + dxPortion - cuboidPoints[0].x < consts.MIN_EDGE_LENGTH
) { ) {
x1 = cuboidPoints[edgeTopIndex].x; x1 = cuboidPoints[edgeTopIndex].x;
x2 = cuboidPoints[edgeBottomIndex].x; x2 = cuboidPoints[edgeBottomIndex].x;
@ -503,7 +522,8 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
this.face.plot(this.cuboidModel.front.points); this.face.plot(this.cuboidModel.front.points);
this.fire(new CustomEvent('resizing', event)); this.fire(new CustomEvent('resizing', event));
}).on('resizedone', (event: CustomEvent) => { })
.on('resizedone', (event: CustomEvent) => {
this.fire(new CustomEvent('resizedone', event)); this.fire(new CustomEvent('resizedone', event));
}); });
@ -544,7 +564,8 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
accumulatedOffset.y = 0; accumulatedOffset.y = 0;
resizedCubePoint = getResizedPointIndex(event) + (orientation === Orientation.LEFT ? 4 : 6); resizedCubePoint = getResizedPointIndex(event) + (orientation === Orientation.LEFT ? 4 : 6);
this.fire(new CustomEvent('resizestart', event)); this.fire(new CustomEvent('resizestart', event));
}).on('resizing', (event: CustomEvent) => { })
.on('resizing', (event: CustomEvent) => {
let { dy } = event.detail; let { dy } = event.detail;
let dyPortion = dy - accumulatedOffset.y; let dyPortion = dy - accumulatedOffset.y;
accumulatedOffset.y += dyPortion; accumulatedOffset.y += dyPortion;
@ -568,7 +589,8 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
const midPointUp = { ...cuboidPoints[edgeTopIndex] }; const midPointUp = { ...cuboidPoints[edgeTopIndex] };
const midPointDown = { ...cuboidPoints[edgeBottomIndex] }; const midPointDown = { ...cuboidPoints[edgeBottomIndex] };
(edgeTopIndex === resizedCubePoint ? midPointUp : midPointDown).y += dyPortion; (edgeTopIndex === resizedCubePoint ? midPointUp : midPointDown).y += dyPortion;
const dorselEdge = (orientation === Orientation.LEFT ? this.cuboidModel.dr : this.cuboidModel.dl); const dorselEdge =
orientation === Orientation.LEFT ? this.cuboidModel.dr : this.cuboidModel.dl;
const constraints = computeSideEdgeConstraints(dorselEdge, this.cuboidModel.fr); const constraints = computeSideEdgeConstraints(dorselEdge, this.cuboidModel.fr);
midPointUp.y = clamp(midPointUp.y, constraints.y1Range.min, constraints.y1Range.max); midPointUp.y = clamp(midPointUp.y, constraints.y1Range.min, constraints.y1Range.max);
midPointDown.y = clamp(midPointDown.y, constraints.y2Range.min, constraints.y2Range.max); midPointDown.y = clamp(midPointDown.y, constraints.y2Range.min, constraints.y2Range.max);
@ -576,11 +598,11 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
this.updateViewAndVM(edge === EdgeIndex.DL); this.updateViewAndVM(edge === EdgeIndex.DL);
} }
this.updateViewAndVM(false); this.updateViewAndVM(false);
this.face.plot(this.cuboidModel.front.points); this.face.plot(this.cuboidModel.front.points);
this.fire(new CustomEvent('resizing', event)); this.fire(new CustomEvent('resizing', event));
}).on('resizedone', (event: CustomEvent) => { })
.on('resizedone', (event: CustomEvent) => {
this.fire(new CustomEvent('resizedone', event)); this.fire(new CustomEvent('resizedone', event));
}); });
} }
@ -608,20 +630,24 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
updatingFace.points = [leftPoints, { x: midX, y: midY }, rightPoints, null]; updatingFace.points = [leftPoints, { x: midX, y: midY }, rightPoints, null];
} }
this.drCenter
this.drCenter.draggable((x: number) => { .draggable((x: number) => {
let xStatus; let xStatus;
if (this.drCenter.cx() < this.cuboidModel.fr.points[0].x) { if (this.drCenter.cx() < this.cuboidModel.fr.points[0].x) {
xStatus = x < this.cuboidModel.fr.points[0].x - consts.MIN_EDGE_LENGTH xStatus =
&& x > this.cuboidModel.vpr.x + consts.MIN_EDGE_LENGTH; x < this.cuboidModel.fr.points[0].x - consts.MIN_EDGE_LENGTH &&
x > this.cuboidModel.vpr.x + consts.MIN_EDGE_LENGTH;
} else { } else {
xStatus = x > this.cuboidModel.fr.points[0].x + consts.MIN_EDGE_LENGTH xStatus =
&& x < this.cuboidModel.vpr.x - consts.MIN_EDGE_LENGTH; x > this.cuboidModel.fr.points[0].x + consts.MIN_EDGE_LENGTH &&
x < this.cuboidModel.vpr.x - consts.MIN_EDGE_LENGTH;
} }
return { x: xStatus, y: this.drCenter.attr('y1') }; return { x: xStatus, y: this.drCenter.attr('y1') };
}).on('dragstart', ((event: CustomEvent) => { })
.on('dragstart', (event: CustomEvent) => {
this.fire(new CustomEvent('resizestart', event)); this.fire(new CustomEvent('resizestart', event));
})).on('dragmove', (event: CustomEvent) => { })
.on('dragmove', (event: CustomEvent) => {
this.dorsalRightEdge.center(this.drCenter.cx(), this.drCenter.cy()); this.dorsalRightEdge.center(this.drCenter.cx(), this.drCenter.cy());
const x = this.dorsalRightEdge.attr('x1'); const x = this.dorsalRightEdge.attr('x1');
@ -633,23 +659,29 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
this.cuboidModel.dr.points = [topPoint, botPoint]; this.cuboidModel.dr.points = [topPoint, botPoint];
this.updateViewAndVM(); this.updateViewAndVM();
this.fire(new CustomEvent('resizing', event)); this.fire(new CustomEvent('resizing', event));
}).on('dragend', (event: CustomEvent) => { })
.on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('resizedone', event)); this.fire(new CustomEvent('resizedone', event));
}); });
this.dlCenter.draggable((x: number) => { this.dlCenter
.draggable((x: number) => {
let xStatus; let xStatus;
if (this.dlCenter.cx() < this.cuboidModel.fl.points[0].x) { if (this.dlCenter.cx() < this.cuboidModel.fl.points[0].x) {
xStatus = x < this.cuboidModel.fl.points[0].x - consts.MIN_EDGE_LENGTH xStatus =
&& x > this.cuboidModel.vpr.x + consts.MIN_EDGE_LENGTH; x < this.cuboidModel.fl.points[0].x - consts.MIN_EDGE_LENGTH &&
x > this.cuboidModel.vpr.x + consts.MIN_EDGE_LENGTH;
} else { } else {
xStatus = x > this.cuboidModel.fl.points[0].x + consts.MIN_EDGE_LENGTH xStatus =
&& x < this.cuboidModel.vpr.x - consts.MIN_EDGE_LENGTH; x > this.cuboidModel.fl.points[0].x + consts.MIN_EDGE_LENGTH &&
x < this.cuboidModel.vpr.x - consts.MIN_EDGE_LENGTH;
} }
return { x: xStatus, y: this.dlCenter.attr('y1') }; return { x: xStatus, y: this.dlCenter.attr('y1') };
}).on('dragstart', ((event: CustomEvent) => { })
.on('dragstart', (event: CustomEvent) => {
this.fire(new CustomEvent('resizestart', event)); this.fire(new CustomEvent('resizestart', event));
})).on('dragmove', (event: CustomEvent) => { })
.on('dragmove', (event: CustomEvent) => {
this.dorsalLeftEdge.center(this.dlCenter.cx(), this.dlCenter.cy()); this.dorsalLeftEdge.center(this.dlCenter.cx(), this.dlCenter.cy());
const x = this.dorsalLeftEdge.attr('x1'); const x = this.dorsalLeftEdge.attr('x1');
@ -661,16 +693,20 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
this.cuboidModel.dl.points = [topPoint, botPoint]; this.cuboidModel.dl.points = [topPoint, botPoint];
this.updateViewAndVM(true); this.updateViewAndVM(true);
this.fire(new CustomEvent('resizing', event)); this.fire(new CustomEvent('resizing', event));
}).on('dragend', (event: CustomEvent) => { })
.on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('resizedone', event)); this.fire(new CustomEvent('resizedone', event));
});; });
this.flCenter.draggable((x: number) => { this.flCenter
.draggable((x: number) => {
const vpX = this.flCenter.cx() - this.cuboidModel.vpl.x > 0 ? this.cuboidModel.vpl.x : 0; const vpX = this.flCenter.cx() - this.cuboidModel.vpl.x > 0 ? this.cuboidModel.vpl.x : 0;
return { x: x < this.cuboidModel.fr.points[0].x && x > vpX + consts.MIN_EDGE_LENGTH }; return { x: x < this.cuboidModel.fr.points[0].x && x > vpX + consts.MIN_EDGE_LENGTH };
}).on('dragstart', ((event: CustomEvent) => { })
.on('dragstart', (event: CustomEvent) => {
this.fire(new CustomEvent('resizestart', event)); this.fire(new CustomEvent('resizestart', event));
})).on('dragmove', (event: CustomEvent) => { })
.on('dragmove', (event: CustomEvent) => {
this.frontLeftEdge.center(this.flCenter.cx(), this.flCenter.cy()); this.frontLeftEdge.center(this.flCenter.cx(), this.flCenter.cy());
const x = this.frontLeftEdge.attr('x1'); const x = this.frontLeftEdge.attr('x1');
@ -682,15 +718,19 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
this.cuboidModel.fl.points = [topPoint, botPoint]; this.cuboidModel.fl.points = [topPoint, botPoint];
this.updateViewAndVM(); this.updateViewAndVM();
this.fire(new CustomEvent('resizing', event)); this.fire(new CustomEvent('resizing', event));
}).on('dragend', (event: CustomEvent) => { })
.on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('resizedone', event)); this.fire(new CustomEvent('resizedone', event));
}); });
this.frCenter.draggable((x: number) => { this.frCenter
.draggable((x: number) => {
return { x: x > this.cuboidModel.fl.points[0].x, y: this.frCenter.attr('y1') }; return { x: x > this.cuboidModel.fl.points[0].x, y: this.frCenter.attr('y1') };
}).on('dragstart', ((event: CustomEvent) => { })
.on('dragstart', (event: CustomEvent) => {
this.fire(new CustomEvent('resizestart', event)); this.fire(new CustomEvent('resizestart', event));
})).on('dragmove', (event: CustomEvent) => { })
.on('dragmove', (event: CustomEvent) => {
this.frontRightEdge.center(this.frCenter.cx(), this.frCenter.cy()); this.frontRightEdge.center(this.frCenter.cx(), this.frCenter.cy());
const x = this.frontRightEdge.attr('x1'); const x = this.frontRightEdge.attr('x1');
@ -702,43 +742,61 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
this.cuboidModel.fr.points = [topPoint, botPoint]; this.cuboidModel.fr.points = [topPoint, botPoint];
this.updateViewAndVM(true); this.updateViewAndVM(true);
this.fire(new CustomEvent('resizing', event)); this.fire(new CustomEvent('resizing', event));
}).on('dragend', (event: CustomEvent) => { })
.on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('resizedone', event)); this.fire(new CustomEvent('resizedone', event));
}); });
this.ftCenter.draggable((x: number, y: number) => { this.ftCenter
.draggable((x: number, y: number) => {
return { x: x === this.ftCenter.cx(), y: y < this.fbCenter.cy() - consts.MIN_EDGE_LENGTH }; return { x: x === this.ftCenter.cx(), y: y < this.fbCenter.cy() - consts.MIN_EDGE_LENGTH };
}).on('dragstart', ((event: CustomEvent) => { })
.on('dragstart', (event: CustomEvent) => {
this.fire(new CustomEvent('resizestart', event)); this.fire(new CustomEvent('resizestart', event));
})).on('dragmove', (event: CustomEvent) => { })
.on('dragmove', (event: CustomEvent) => {
this.frontTopEdge.center(this.ftCenter.cx(), this.ftCenter.cy()); this.frontTopEdge.center(this.ftCenter.cx(), this.ftCenter.cy());
horizontalEdgeControl.call(this, this.cuboidModel.top, this.frontTopEdge.attr('x2'), this.frontTopEdge.attr('y2')); horizontalEdgeControl.call(
this,
this.cuboidModel.top,
this.frontTopEdge.attr('x2'),
this.frontTopEdge.attr('y2'),
);
this.updateViewAndVM(); this.updateViewAndVM();
this.fire(new CustomEvent('resizing', event)); this.fire(new CustomEvent('resizing', event));
}).on('dragend', (event: CustomEvent) => { })
.on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('resizedone', event)); this.fire(new CustomEvent('resizedone', event));
}); });
this.fbCenter.draggable((x: number, y: number) => { this.fbCenter
.draggable((x: number, y: number) => {
return { x: x === this.fbCenter.cx(), y: y > this.ftCenter.cy() + consts.MIN_EDGE_LENGTH }; return { x: x === this.fbCenter.cx(), y: y > this.ftCenter.cy() + consts.MIN_EDGE_LENGTH };
}).on('dragstart', ((event: CustomEvent) => { })
.on('dragstart', (event: CustomEvent) => {
this.fire(new CustomEvent('resizestart', event)); this.fire(new CustomEvent('resizestart', event));
})).on('dragmove', (event: CustomEvent) => { })
.on('dragmove', (event: CustomEvent) => {
this.frontBotEdge.center(this.fbCenter.cx(), this.fbCenter.cy()); this.frontBotEdge.center(this.fbCenter.cx(), this.fbCenter.cy());
horizontalEdgeControl.call(this, this.cuboidModel.bot, this.frontBotEdge.attr('x2'), this.frontBotEdge.attr('y2')); horizontalEdgeControl.call(
this,
this.cuboidModel.bot,
this.frontBotEdge.attr('x2'),
this.frontBotEdge.attr('y2'),
);
this.updateViewAndVM(); this.updateViewAndVM();
this.fire(new CustomEvent('resizing', event)); this.fire(new CustomEvent('resizing', event));
}).on('dragend', (event: CustomEvent) => { })
.on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('resizedone', event)); this.fire(new CustomEvent('resizedone', event));
}); });
return this; return this;
}, },
draggable(value: any, constraint: any) { draggable(value: any, constraint: any) {
const { cuboidModel } = this; const { cuboidModel } = this;
const faces = [this.face, this.right, this.dorsal, this.left] const faces = [this.face, this.right, this.dorsal, this.left];
const accumulatedOffset: Point = { const accumulatedOffset: Point = {
x: 0, x: 0,
y: 0, y: 0,
@ -750,16 +808,19 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
face.off('dragstart'); face.off('dragstart');
face.off('dragmove'); face.off('dragmove');
face.off('dragend'); face.off('dragend');
}) });
return return;
} }
this.face.draggable().on('dragstart', (event: CustomEvent) => { this.face
.draggable()
.on('dragstart', (event: CustomEvent) => {
accumulatedOffset.x = 0; accumulatedOffset.x = 0;
accumulatedOffset.y = 0; accumulatedOffset.y = 0;
this.fire(new CustomEvent('dragstart', event)); this.fire(new CustomEvent('dragstart', event));
}).on('dragmove', (event: CustomEvent) => { })
.on('dragmove', (event: CustomEvent) => {
const dx = event.detail.p.x - event.detail.handler.startPoints.point.x; const dx = event.detail.p.x - event.detail.handler.startPoints.point.x;
const dy = event.detail.p.y - event.detail.handler.startPoints.point.y; const dy = event.detail.p.y - event.detail.handler.startPoints.point.y;
let dxPortion = dx - accumulatedOffset.x; let dxPortion = dx - accumulatedOffset.x;
@ -770,47 +831,59 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
this.dmove(dxPortion, dyPortion); this.dmove(dxPortion, dyPortion);
this.fire(new CustomEvent('dragmove', event)); this.fire(new CustomEvent('dragmove', event));
}).on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('dragend', event));
}) })
.on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('dragend', event));
});
this.left.draggable((x: number, y: number) => ({ this.left
x: x < Math.min(cuboidModel.dr.points[0].x, .draggable((x: number, y: number) => ({
cuboidModel.fr.points[0].x) - consts.MIN_EDGE_LENGTH, y x: x < Math.min(cuboidModel.dr.points[0].x, cuboidModel.fr.points[0].x) - consts.MIN_EDGE_LENGTH,
})).on('dragstart', (event: CustomEvent) => { y,
}))
.on('dragstart', (event: CustomEvent) => {
this.fire(new CustomEvent('dragstart', event)); this.fire(new CustomEvent('dragstart', event));
}).on('dragmove', (event: CustomEvent) => { })
.on('dragmove', (event: CustomEvent) => {
this.cuboidModel.left.points = parsePoints(this.left.attr('points')); this.cuboidModel.left.points = parsePoints(this.left.attr('points'));
this.updateViewAndVM(); this.updateViewAndVM();
this.fire(new CustomEvent('dragmove', event)); this.fire(new CustomEvent('dragmove', event));
}).on('dragend', (event: CustomEvent) => { })
.on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('dragend', event)); this.fire(new CustomEvent('dragend', event));
}); });
this.dorsal.draggable().on('dragstart', (event: CustomEvent) => { this.dorsal
.draggable()
.on('dragstart', (event: CustomEvent) => {
this.fire(new CustomEvent('dragstart', event)); this.fire(new CustomEvent('dragstart', event));
}).on('dragmove', (event: CustomEvent) => { })
.on('dragmove', (event: CustomEvent) => {
this.cuboidModel.dorsal.points = parsePoints(this.dorsal.attr('points')); this.cuboidModel.dorsal.points = parsePoints(this.dorsal.attr('points'));
this.updateViewAndVM(); this.updateViewAndVM();
this.fire(new CustomEvent('dragmove', event)); this.fire(new CustomEvent('dragmove', event));
}).on('dragend', (event: CustomEvent) => { })
.on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('dragend', event)); this.fire(new CustomEvent('dragend', event));
}); });
this.right.draggable((x: number, y: number) => ({ this.right
x: x > Math.min(cuboidModel.dl.points[0].x, .draggable((x: number, y: number) => ({
cuboidModel.fl.points[0].x) + consts.MIN_EDGE_LENGTH, y x: x > Math.min(cuboidModel.dl.points[0].x, cuboidModel.fl.points[0].x) + consts.MIN_EDGE_LENGTH,
})).on('dragstart', (event: CustomEvent) => { y,
}))
.on('dragstart', (event: CustomEvent) => {
this.fire(new CustomEvent('dragstart', event)); this.fire(new CustomEvent('dragstart', event));
}).on('dragmove', (event: CustomEvent) => { })
.on('dragmove', (event: CustomEvent) => {
this.cuboidModel.right.points = parsePoints(this.right.attr('points')); this.cuboidModel.right.points = parsePoints(this.right.attr('points'));
this.updateViewAndVM(true); this.updateViewAndVM(true);
this.fire(new CustomEvent('dragmove', event)); this.fire(new CustomEvent('dragmove', event));
}).on('dragend', (event: CustomEvent) => { })
.on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('dragend', event)); this.fire(new CustomEvent('dragend', event));
}); });
@ -820,8 +893,7 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
_attr: SVG.Element.prototype.attr, _attr: SVG.Element.prototype.attr,
attr(a: any, v: any, n: any) { attr(a: any, v: any, n: any) {
if ((a === 'fill' || a === 'stroke' || a === 'face-stroke') if ((a === 'fill' || a === 'stroke' || a === 'face-stroke') && v !== undefined) {
&& v !== undefined) {
this._attr(a, v, n); this._attr(a, v, n);
this.paintOrientationLines(); this.paintOrientationLines();
} else if (a === 'points' && typeof v === 'string') { } else if (a === 'points' && typeof v === 'string') {
@ -841,13 +913,16 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
this.rtProj.hide(); this.rtProj.hide();
this.rbProj.hide(); this.rbProj.hide();
} }
} else if (a === 'stroke-width' && typeof v === "number") { } else if (a === 'stroke-width' && typeof v === 'number') {
this._attr(a, v, n); this._attr(a, v, n);
this.updateThickness(); this.updateThickness();
} else if (a === 'data-z-order' && typeof v !== 'undefined') { } else if (a === 'data-z-order' && typeof v !== 'undefined') {
this._attr(a, v, n); this._attr(a, v, n);
[this.face, this.left, this.dorsal, this.right, ...this.getEdges(), ...this.getGrabPoints()] [this.face, this.left, this.dorsal, this.right, ...this.getEdges(), ...this.getGrabPoints()].forEach(
.forEach((el) => {if (el) el.attr(a, v, n)}) (el) => {
if (el) el.attr(a, v, n);
},
);
} else { } else {
return this._attr(a, v, n); return this._attr(a, v, n);
} }
@ -856,7 +931,7 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
}, },
updateThickness() { updateThickness() {
const edges = [this.frontLeftEdge, this.frontRightEdge, this.frontTopEdge, this.frontBotEdge] const edges = [this.frontLeftEdge, this.frontRightEdge, this.frontTopEdge, this.frontBotEdge];
const width = this.attr('stroke-width'); const width = this.attr('stroke-width');
edges.forEach((edge: SVG.Element) => { edges.forEach((edge: SVG.Element) => {
edge.attr('stroke-width', width * (this.strokeOffset || consts.CUBOID_UNACTIVE_EDGE_STROKE_WIDTH)); edge.attr('stroke-width', width * (this.strokeOffset || consts.CUBOID_UNACTIVE_EDGE_STROKE_WIDTH));
@ -864,14 +939,15 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
this.on('mouseover', () => { this.on('mouseover', () => {
edges.forEach((edge: SVG.Element) => { edges.forEach((edge: SVG.Element) => {
this.strokeOffset = this.node.classList.contains('cvat_canvas_shape_activated') this.strokeOffset = this.node.classList.contains('cvat_canvas_shape_activated')
? consts.CUBOID_ACTIVE_EDGE_STROKE_WIDTH : consts.CUBOID_UNACTIVE_EDGE_STROKE_WIDTH; ? consts.CUBOID_ACTIVE_EDGE_STROKE_WIDTH
: consts.CUBOID_UNACTIVE_EDGE_STROKE_WIDTH;
edge.attr('stroke-width', width * this.strokeOffset); edge.attr('stroke-width', width * this.strokeOffset);
}) });
}).on('mouseout', () => { }).on('mouseout', () => {
edges.forEach((edge: SVG.Element) => { edges.forEach((edge: SVG.Element) => {
this.strokeOffset = consts.CUBOID_UNACTIVE_EDGE_STROKE_WIDTH; this.strokeOffset = consts.CUBOID_UNACTIVE_EDGE_STROKE_WIDTH;
edge.attr('stroke-width', width * this.strokeOffset); edge.attr('stroke-width', width * this.strokeOffset);
}) });
}); });
}, },
@ -889,18 +965,12 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
this.dorsalRightEdge.stroke({ color: strokeColor }); this.dorsalRightEdge.stroke({ color: strokeColor });
this.dorsalLeftEdge.stroke({ color: strokeColor }); this.dorsalLeftEdge.stroke({ color: strokeColor });
this.bot.stroke({ color: strokeColor }) this.bot.stroke({ color: strokeColor }).fill({ color: fillColor });
.fill({ color: fillColor }); this.top.stroke({ color: strokeColor }).fill({ color: fillColor });
this.top.stroke({ color: strokeColor }) this.face.stroke({ color: strokeColor, width: 0 }).fill({ color: fillColor });
.fill({ color: fillColor }); this.right.stroke({ color: strokeColor }).fill({ color: fillColor });
this.face.stroke({ color: strokeColor, width: 0 }) this.dorsal.stroke({ color: strokeColor }).fill({ color: fillColor });
.fill({ color: fillColor }); this.left.stroke({ color: strokeColor }).fill({ color: fillColor });
this.right.stroke({ color: strokeColor })
.fill({ color: fillColor });
this.dorsal.stroke({ color: strokeColor })
.fill({ color: fillColor });
this.left.stroke({ color: strokeColor })
.fill({ color: fillColor });
}, },
dmove(dx: number, dy: number) { dmove(dx: number, dy: number) {
@ -954,9 +1024,13 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
this.updateView(); this.updateView();
// to correct getting of points in resizedone, dragdone // to correct getting of points in resizedone, dragdone
this._attr('points', this.cuboidModel this._attr(
'points',
this.cuboidModel
.getPoints() .getPoints()
.reduce((acc: string, point: Point): string => `${acc} ${point.x},${point.y}`, '').trim()); .reduce((acc: string, point: Point): string => `${acc} ${point.x},${point.y}`, '')
.trim(),
);
}, },
computeHeightFace(point: Point, index: number) { computeHeightFace(point: Point, index: number) {
@ -1037,14 +1111,18 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
updateProjections() { updateProjections() {
const viewModel = this.cuboidModel; const viewModel = this.cuboidModel;
this.ftProj.plot(this.updateProjectionLine(viewModel.ft.getEquation(), this.ftProj.plot(
viewModel.ft.points[0], viewModel.vpl)); this.updateProjectionLine(viewModel.ft.getEquation(), viewModel.ft.points[0], viewModel.vpl),
this.fbProj.plot(this.updateProjectionLine(viewModel.fb.getEquation(), );
viewModel.ft.points[0], viewModel.vpl)); this.fbProj.plot(
this.rtProj.plot(this.updateProjectionLine(viewModel.rt.getEquation(), this.updateProjectionLine(viewModel.fb.getEquation(), viewModel.ft.points[0], viewModel.vpl),
viewModel.rt.points[1], viewModel.vpr)); );
this.rbProj.plot(this.updateProjectionLine(viewModel.rb.getEquation(), this.rtProj.plot(
viewModel.rt.points[1], viewModel.vpr)); this.updateProjectionLine(viewModel.rt.getEquation(), viewModel.rt.points[1], viewModel.vpr),
);
this.rbProj.plot(
this.updateProjectionLine(viewModel.rb.getEquation(), viewModel.rt.points[1], viewModel.vpr),
);
}, },
updateGrabPoints() { updateGrabPoints() {

@ -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],

@ -15,7 +15,5 @@
"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$/, test: /\.ts$/,
exclude: /node_modules/, exclude: /node_modules/,
use: { use: {
loader: 'babel-loader', loader: 'babel-loader',
options: { options: {
plugins: ['@babel/plugin-proposal-class-properties'], plugins: [
presets: [ '@babel/plugin-proposal-class-properties',
['@babel/preset-env'], '@babel/plugin-proposal-optional-chaining',
['@babel/typescript'],
], ],
presets: [['@babel/preset-env'], ['@babel/typescript']],
sourceType: 'unambiguous', sourceType: 'unambiguous',
}, },
}, },
}, { },
{
test: /\.(css|scss)$/, test: /\.(css|scss)$/,
exclude: /node_modules/, exclude: /node_modules/,
use: ['style-loader', { use: [
'style-loader',
{
loader: 'css-loader', loader: 'css-loader',
options: { options: {
importLoaders: 2, importLoaders: 2,
}, },
}, 'postcss-loader', 'sass-loader'] },
}], '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,7 +88,8 @@ const webConfig = {
extensions: ['.ts', '.js', '.json'], extensions: ['.ts', '.js', '.json'],
}, },
module: { module: {
rules: [{ rules: [
{
test: /\.ts$/, test: /\.ts$/,
exclude: /node_modules/, exclude: /node_modules/,
use: { use: {
@ -87,24 +97,34 @@ const webConfig = {
options: { options: {
plugins: ['@babel/plugin-proposal-class-properties'], plugins: ['@babel/plugin-proposal-class-properties'],
presets: [ presets: [
['@babel/preset-env', { [
'@babel/preset-env',
{
targets: '> 2.5%', // https://github.com/browserslist/browserslist targets: '> 2.5%', // https://github.com/browserslist/browserslist
}], },
],
['@babel/typescript'], ['@babel/typescript'],
], ],
sourceType: 'unambiguous', sourceType: 'unambiguous',
}, },
}, },
}, { },
{
test: /\.scss$/, test: /\.scss$/,
exclude: /node_modules/, exclude: /node_modules/,
use: ['style-loader', { use: [
'style-loader',
{
loader: 'css-loader', loader: 'css-loader',
options: { options: {
importLoaders: 2, importLoaders: 2,
}, },
}, 'postcss-loader', 'sass-loader'] },
}], '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: ['security', 'jest', 'no-unsafe-innerhtml', 'no-unsanitized'],
extends: ['eslint:recommended', 'plugin:security/recommended', 'plugin:no-unsanitized/DOM', 'airbnb-base'],
rules: {
'no-await-in-loop': [0],
'global-require': [0],
'no-new': [0],
'class-methods-use-this': [0],
'no-restricted-properties': [
0,
{
object: 'Math',
property: 'pow',
}, },
"plugins": [
"security",
"no-unsanitized",
"no-unsafe-innerhtml",
],
"extends": [
"eslint:recommended",
"plugin:security/recommended",
"plugin:no-unsanitized/DOM",
"airbnb-base",
], ],
"rules": { 'no-plusplus': [0],
"no-await-in-loop": [0], 'no-param-reassign': [0],
"global-require": [0], 'no-underscore-dangle': ['error', { allowAfterThis: true }],
"no-new": [0], 'no-restricted-syntax': [0, { selector: 'ForOfStatement' }],
"class-methods-use-this": [0], 'no-continue': [0],
"no-restricted-properties": [0, { 'no-unsafe-innerhtml/no-unsafe-innerhtml': 1,
"object": "Math",
"property": "pow",
}],
"no-plusplus": [0],
"no-param-reassign": [0],
"no-underscore-dangle": ["error", { "allowAfterThis": true }],
"no-restricted-syntax": [0, {"selector": "ForOfStatement"}],
"no-continue": [0],
"no-unsafe-innerhtml/no-unsafe-innerhtml": 1,
// This rule actual for user input data on the node.js environment mainly. // 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,7 +1,6 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
(() => { (() => {
/** /**

@ -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)
.reduce((accumulator, value) => {
accumulator.push(...value); accumulator.push(...value);
return accumulator; return accumulator;
}, []).filter((tag) => !tag.removed) }, [])
.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,13 +373,15 @@
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,
@ -426,20 +402,23 @@
object.removed = true; object.removed = true;
} }
this.history.do(HistoryActions.MERGED_OBJECTS, () => { this.history.do(
HistoryActions.MERGED_OBJECTS,
() => {
trackModel.removed = true; trackModel.removed = true;
for (const object of objectsForMerge) { for (const object of objectsForMerge) {
object.removed = false; object.removed = false;
} }
}, () => { },
() => {
trackModel.removed = false; trackModel.removed = false;
for (const object of objectsForMerge) { for (const object of objectsForMerge) {
object.removed = true; object.removed = true;
} }
}, [ },
...objectsForMerge [...objectsForMerge.map((object) => object.clientID), trackModel.clientID],
.map((object) => object.clientID), trackModel.clientID, objectStates[0].frame,
], 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,8 +451,7 @@
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,
@ -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(
HistoryActions.SPLITTED_TRACK,
() => {
object.removed = false; object.removed = false;
prevTrack.removed = true; prevTrack.removed = true;
nextTrack.removed = true; nextTrack.removed = true;
}, () => { },
() => {
object.removed = true; object.removed = true;
prevTrack.removed = false; prevTrack.removed = false;
nextTrack.removed = false; nextTrack.removed = false;
}, [object.clientID, prevTrack.clientID, nextTrack.clientID], frame); },
[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(
HistoryActions.GROUPED_OBJECTS,
() => {
objectsForGroup.forEach((object, idx) => { objectsForGroup.forEach((object, idx) => {
object.group = undoGroups[idx]; object.group = undoGroups[idx];
}); });
}, () => { },
() => {
objectsForGroup.forEach((object, idx) => { objectsForGroup.forEach((object, idx) => {
object.group = redoGroups[idx]; object.group = redoGroups[idx];
}); });
}, objectsForGroup.map((object) => object.clientID), objectStates[0].frame); },
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(
HistoryActions.CREATED_OBJECTS,
() => {
importedArray.forEach((object) => { importedArray.forEach((object) => {
object.removed = true; object.removed = true;
}); });
}, () => { },
() => {
importedArray.forEach((object) => { importedArray.forEach((object) => {
object.removed = false; object.removed = false;
}); });
}, importedArray.map((object) => object.clientID), objectStates[0].frame); },
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,8 +140,7 @@ 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;
}, {}); }, {});
@ -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,

@ -1,32 +1,15 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019-2020 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
(() => { (() => {
const ObjectState = require('./object-state'); const ObjectState = require('./object-state');
const { checkObjectType } = require('./common');
const { const {
checkObjectType, colors, Source, ObjectShape, ObjectType, AttributeType, HistoryActions,
} = require('./common');
const {
colors,
Source,
ObjectShape,
ObjectType,
AttributeType,
HistoryActions,
} = require('./enums'); } = require('./enums');
const { const { DataError, ArgumentError, ScriptingError } = require('./exceptions');
DataError,
ArgumentError,
ScriptingError,
} = require('./exceptions');
const { Label } = require('./labels'); const { Label } = require('./labels');
@ -48,38 +31,26 @@
function checkNumberOfPoints(shapeType, points) { function checkNumberOfPoints(shapeType, points) {
if (shapeType === ObjectShape.RECTANGLE) { if (shapeType === ObjectShape.RECTANGLE) {
if (points.length / 2 !== 2) { if (points.length / 2 !== 2) {
throw new DataError( throw new DataError(`Rectangle must have 2 points, but got ${points.length / 2}`);
`Rectangle must have 2 points, but got ${points.length / 2}`,
);
} }
} else if (shapeType === ObjectShape.POLYGON) { } else if (shapeType === ObjectShape.POLYGON) {
if (points.length / 2 < 3) { if (points.length / 2 < 3) {
throw new DataError( throw new DataError(`Polygon must have at least 3 points, but got ${points.length / 2}`);
`Polygon must have at least 3 points, but got ${points.length / 2}`,
);
} }
} else if (shapeType === ObjectShape.POLYLINE) { } else if (shapeType === ObjectShape.POLYLINE) {
if (points.length / 2 < 2) { if (points.length / 2 < 2) {
throw new DataError( throw new DataError(`Polyline must have at least 2 points, but got ${points.length / 2}`);
`Polyline must have at least 2 points, but got ${points.length / 2}`,
);
} }
} else if (shapeType === ObjectShape.POINTS) { } else if (shapeType === ObjectShape.POINTS) {
if (points.length / 2 < 1) { if (points.length / 2 < 1) {
throw new DataError( throw new DataError(`Points must have at least 1 points, but got ${points.length / 2}`);
`Points must have at least 1 points, but got ${points.length / 2}`,
);
} }
} else if (shapeType === ObjectShape.CUBOID) { } else if (shapeType === ObjectShape.CUBOID) {
if (points.length / 2 !== 8) { if (points.length / 2 !== 8) {
throw new DataError( throw new DataError(`Points must have exact 8 points, but got ${points.length / 2}`);
`Points must have exact 8 points, but got ${points.length / 2}`,
);
} }
} else { } else {
throw new ArgumentError( throw new ArgumentError(`Unknown value of shapeType has been recieved ${shapeType}`);
`Unknown value of shapeType has been recieved ${shapeType}`,
);
} }
} }
@ -104,10 +75,7 @@
} }
if (shapeType === ObjectShape.POLYLINE) { if (shapeType === ObjectShape.POLYLINE) {
const length = Math.max( const length = Math.max(xmax - xmin, ymax - ymin);
xmax - xmin,
ymax - ymin,
);
return length >= MIN_SHAPE_LENGTH; return length >= MIN_SHAPE_LENGTH;
} }
@ -126,10 +94,7 @@
checkObjectType('coordinate', x, 'number', null); checkObjectType('coordinate', x, 'number', null);
checkObjectType('coordinate', y, 'number', null); checkObjectType('coordinate', y, 'number', null);
fittedPoints.push( fittedPoints.push(Math.clamp(x, 0, maxX), Math.clamp(y, 0, maxY));
Math.clamp(x, 0, maxX),
Math.clamp(y, 0, maxY),
);
} }
return shapeType === ObjectShape.CUBOID ? points : fittedPoints; return shapeType === ObjectShape.CUBOID ? points : fittedPoints;
@ -149,15 +114,12 @@
const { values } = attr; const { values } = attr;
const type = attr.inputType; const type = attr.inputType;
if (typeof (value) !== 'string') { if (typeof value !== 'string') {
throw new ArgumentError( throw new ArgumentError(`Attribute value is expected to be string, but got ${typeof value}`);
`Attribute value is expected to be string, but got ${typeof (value)}`,
);
} }
if (type === AttributeType.NUMBER) { if (type === AttributeType.NUMBER) {
return +value >= +values[0] return +value >= +values[0] && +value <= +values[1];
&& +value <= +values[1];
} }
if (type === AttributeType.CHECKBOX) { if (type === AttributeType.CHECKBOX) {
@ -190,17 +152,18 @@
attributeAccumulator[attr.spec_id] = attr.value; attributeAccumulator[attr.spec_id] = attr.value;
return attributeAccumulator; return attributeAccumulator;
}, {}); }, {});
this.groupObject = Object.defineProperties({}, { this.groupObject = Object.defineProperties(
{},
{
color: { color: {
get: () => { get: () => {
if (this.group) { if (this.group) {
return this.groupColors[this.group] return this.groupColors[this.group] || colors[this.group % colors.length];
|| colors[this.group % colors.length];
} }
return defaultGroupColor; return defaultGroupColor;
}, },
set: (newColor) => { set: (newColor) => {
if (this.group && typeof (newColor) === 'string' && /^#[0-9A-F]{6}$/i.test(newColor)) { if (this.group && typeof newColor === 'string' && /^#[0-9A-F]{6}$/i.test(newColor)) {
this.groupColors[this.group] = newColor; this.groupColors[this.group] = newColor;
} }
}, },
@ -208,7 +171,8 @@
id: { id: {
get: () => this.group, get: () => this.group,
}, },
}); },
);
this.appendDefaultAttributes(this.label); this.appendDefaultAttributes(this.label);
injection.groups.max = Math.max(injection.groups.max, this.group); injection.groups.max = Math.max(injection.groups.max, this.group);
@ -218,13 +182,19 @@
const undoLock = this.lock; const undoLock = this.lock;
const redoLock = lock; const redoLock = lock;
this.history.do(HistoryActions.CHANGED_LOCK, () => { this.history.do(
HistoryActions.CHANGED_LOCK,
() => {
this.lock = undoLock; this.lock = undoLock;
this.updated = Date.now(); this.updated = Date.now();
}, () => { },
() => {
this.lock = redoLock; this.lock = redoLock;
this.updated = Date.now(); this.updated = Date.now();
}, [this.clientID], frame); },
[this.clientID],
frame,
);
this.lock = lock; this.lock = lock;
} }
@ -233,13 +203,19 @@
const undoColor = this.color; const undoColor = this.color;
const redoColor = color; const redoColor = color;
this.history.do(HistoryActions.CHANGED_COLOR, () => { this.history.do(
HistoryActions.CHANGED_COLOR,
() => {
this.color = undoColor; this.color = undoColor;
this.updated = Date.now(); this.updated = Date.now();
}, () => { },
() => {
this.color = redoColor; this.color = redoColor;
this.updated = Date.now(); this.updated = Date.now();
}, [this.clientID], frame); },
[this.clientID],
frame,
);
this.color = color; this.color = color;
} }
@ -248,13 +224,19 @@
const undoHidden = this.hidden; const undoHidden = this.hidden;
const redoHidden = hidden; const redoHidden = hidden;
this.history.do(HistoryActions.CHANGED_HIDDEN, () => { this.history.do(
HistoryActions.CHANGED_HIDDEN,
() => {
this.hidden = undoHidden; this.hidden = undoHidden;
this.updated = Date.now(); this.updated = Date.now();
}, () => { },
() => {
this.hidden = redoHidden; this.hidden = redoHidden;
this.updated = Date.now(); this.updated = Date.now();
}, [this.clientID], frame); },
[this.clientID],
frame,
);
this.hidden = hidden; this.hidden = hidden;
} }
@ -268,15 +250,21 @@
this.appendDefaultAttributes(label); this.appendDefaultAttributes(label);
const redoAttributes = { ...this.attributes }; const redoAttributes = { ...this.attributes };
this.history.do(HistoryActions.CHANGED_LABEL, () => { this.history.do(
HistoryActions.CHANGED_LABEL,
() => {
this.label = undoLabel; this.label = undoLabel;
this.attributes = undoAttributes; this.attributes = undoAttributes;
this.updated = Date.now(); this.updated = Date.now();
}, () => { },
() => {
this.label = redoLabel; this.label = redoLabel;
this.attributes = redoAttributes; this.attributes = redoAttributes;
this.updated = Date.now(); this.updated = Date.now();
}, [this.clientID], frame); },
[this.clientID],
frame,
);
} }
_saveAttributes(attributes, frame) { _saveAttributes(attributes, frame) {
@ -288,13 +276,19 @@
const redoAttributes = { ...this.attributes }; const redoAttributes = { ...this.attributes };
this.history.do(HistoryActions.CHANGED_ATTRIBUTES, () => { this.history.do(
HistoryActions.CHANGED_ATTRIBUTES,
() => {
this.attributes = undoAttributes; this.attributes = undoAttributes;
this.updated = Date.now(); this.updated = Date.now();
}, () => { },
() => {
this.attributes = redoAttributes; this.attributes = redoAttributes;
this.updated = Date.now(); this.updated = Date.now();
}, [this.clientID], frame); },
[this.clientID],
frame,
);
} }
_validateStateBeforeSave(frame, data, updated) { _validateStateBeforeSave(frame, data, updated) {
@ -304,8 +298,7 @@
checkObjectType('label', data.label, null, Label); checkObjectType('label', data.label, null, Label);
} }
const labelAttributes = data.label.attributes const labelAttributes = data.label.attributes.reduce((accumulator, value) => {
.reduce((accumulator, value) => {
accumulator[value.id] = value; accumulator[value.id] = value;
return accumulator; return accumulator;
}, {}); }, {});
@ -334,9 +327,7 @@
const { width, height } = this.frameMeta[frame]; const { width, height } = this.frameMeta[frame];
fittedPoints = fitPoints(this.shapeType, data.points, width, height); fittedPoints = fitPoints(this.shapeType, data.points, width, height);
if ((!checkShapeArea(this.shapeType, fittedPoints)) if (!checkShapeArea(this.shapeType, fittedPoints) || checkOutside(fittedPoints, width, height)) {
|| checkOutside(fittedPoints, width, height)
) {
fittedPoints = []; fittedPoints = [];
} }
} }
@ -364,9 +355,7 @@
if (updated.color) { if (updated.color) {
checkObjectType('color', data.color, 'string', null); checkObjectType('color', data.color, 'string', null);
if (!/^#[0-9A-F]{6}$/i.test(data.color)) { if (!/^#[0-9A-F]{6}$/i.test(data.color)) {
throw new ArgumentError( throw new ArgumentError(`Got invalid color value: "${data.color}"`);
`Got invalid color value: "${data.color}"`,
);
} }
} }
@ -396,9 +385,16 @@
} }
updateTimestamp(updated) { updateTimestamp(updated) {
const anyChanges = updated.label || updated.attributes || updated.points const anyChanges = updated.label
|| updated.outside || updated.occluded || updated.keyframe || updated.attributes
|| updated.zOrder || updated.hidden || updated.lock || updated.pinned; || updated.points
|| updated.outside
|| updated.occluded
|| updated.keyframe
|| updated.zOrder
|| updated.hidden
|| updated.lock
|| updated.pinned;
if (anyChanges) { if (anyChanges) {
this.updated = Date.now(); this.updated = Date.now();
@ -409,14 +405,20 @@
if (!this.lock || force) { if (!this.lock || force) {
this.removed = true; this.removed = true;
this.history.do(HistoryActions.REMOVED_OBJECT, () => { this.history.do(
HistoryActions.REMOVED_OBJECT,
() => {
this.serverID = undefined; this.serverID = undefined;
this.removed = false; this.removed = false;
this.updated = Date.now(); this.updated = Date.now();
}, () => { },
() => {
this.removed = true; this.removed = true;
this.updated = Date.now(); this.updated = Date.now();
}, [this.clientID], frame); },
[this.clientID],
frame,
);
} }
return this.removed; return this.removed;
@ -436,33 +438,33 @@
const undoPinned = this.pinned; const undoPinned = this.pinned;
const redoPinned = pinned; const redoPinned = pinned;
this.history.do(HistoryActions.CHANGED_PINNED, () => { this.history.do(
HistoryActions.CHANGED_PINNED,
() => {
this.pinned = undoPinned; this.pinned = undoPinned;
this.updated = Date.now(); this.updated = Date.now();
}, () => { },
() => {
this.pinned = redoPinned; this.pinned = redoPinned;
this.updated = Date.now(); this.updated = Date.now();
}, [this.clientID], frame); },
[this.clientID],
frame,
);
this.pinned = pinned; this.pinned = pinned;
} }
save() { save() {
throw new ScriptingError( throw new ScriptingError('Is not implemented');
'Is not implemented',
);
} }
get() { get() {
throw new ScriptingError( throw new ScriptingError('Is not implemented');
'Is not implemented',
);
} }
toJSON() { toJSON() {
throw new ScriptingError( throw new ScriptingError('Is not implemented');
'Is not implemented',
);
} }
} }
@ -501,9 +503,7 @@
// Method is used to construct ObjectState objects // Method is used to construct ObjectState objects
get(frame) { get(frame) {
if (frame !== this.frame) { if (frame !== this.frame) {
throw new ScriptingError( throw new ScriptingError('Got frame is not equal to the frame of the shape');
'Got frame is not equal to the frame of the shape',
);
} }
return { return {
@ -533,15 +533,21 @@
const undoSource = this.source; const undoSource = this.source;
const redoSource = Source.MANUAL; const redoSource = Source.MANUAL;
this.history.do(HistoryActions.CHANGED_POINTS, () => { this.history.do(
HistoryActions.CHANGED_POINTS,
() => {
this.points = undoPoints; this.points = undoPoints;
this.source = undoSource; this.source = undoSource;
this.updated = Date.now(); this.updated = Date.now();
}, () => { },
() => {
this.points = redoPoints; this.points = redoPoints;
this.source = redoSource; this.source = redoSource;
this.updated = Date.now(); this.updated = Date.now();
}, [this.clientID], frame); },
[this.clientID],
frame,
);
this.source = Source.MANUAL; this.source = Source.MANUAL;
this.points = points; this.points = points;
@ -553,15 +559,21 @@
const undoSource = this.source; const undoSource = this.source;
const redoSource = Source.MANUAL; const redoSource = Source.MANUAL;
this.history.do(HistoryActions.CHANGED_OCCLUDED, () => { this.history.do(
HistoryActions.CHANGED_OCCLUDED,
() => {
this.occluded = undoOccluded; this.occluded = undoOccluded;
this.source = undoSource; this.source = undoSource;
this.updated = Date.now(); this.updated = Date.now();
}, () => { },
() => {
this.occluded = redoOccluded; this.occluded = redoOccluded;
this.source = redoSource; this.source = redoSource;
this.updated = Date.now(); this.updated = Date.now();
}, [this.clientID], frame); },
[this.clientID],
frame,
);
this.source = Source.MANUAL; this.source = Source.MANUAL;
this.occluded = occluded; this.occluded = occluded;
@ -573,15 +585,21 @@
const undoSource = this.source; const undoSource = this.source;
const redoSource = Source.MANUAL; const redoSource = Source.MANUAL;
this.history.do(HistoryActions.CHANGED_ZORDER, () => { this.history.do(
HistoryActions.CHANGED_ZORDER,
() => {
this.zOrder = undoZOrder; this.zOrder = undoZOrder;
this.source = undoSource; this.source = undoSource;
this.updated = Date.now(); this.updated = Date.now();
}, () => { },
() => {
this.zOrder = redoZOrder; this.zOrder = redoZOrder;
this.source = redoSource; this.source = redoSource;
this.updated = Date.now(); this.updated = Date.now();
}, [this.clientID], frame); },
[this.clientID],
frame,
);
this.source = Source.MANUAL; this.source = Source.MANUAL;
this.zOrder = zOrder; this.zOrder = zOrder;
@ -589,9 +607,7 @@
save(frame, data) { save(frame, data) {
if (frame !== this.frame) { if (frame !== this.frame) {
throw new ScriptingError( throw new ScriptingError('Got frame is not equal to the frame of the shape');
'Got frame is not equal to the frame of the shape',
);
} }
if (this.lock && data.lock) { if (this.lock && data.lock) {
@ -696,8 +712,8 @@
z_order: this.shapes[frame].zOrder, z_order: this.shapes[frame].zOrder,
points: [...this.shapes[frame].points], points: [...this.shapes[frame].points],
outside: this.shapes[frame].outside, outside: this.shapes[frame].outside,
attributes: Object.keys(this.shapes[frame].attributes) attributes: Object.keys(this.shapes[frame].attributes).reduce(
.reduce((attributeAccumulator, attrId) => { (attributeAccumulator, attrId) => {
if (labelAttributes[attrId].mutable) { if (labelAttributes[attrId].mutable) {
attributeAccumulator.push({ attributeAccumulator.push({
spec_id: attrId, spec_id: attrId,
@ -706,7 +722,9 @@
} }
return attributeAccumulator; return attributeAccumulator;
}, []), },
[],
),
id: this.shapes[frame].serverID, id: this.shapes[frame].serverID,
frame: +frame, frame: +frame,
}); });
@ -719,10 +737,7 @@
// Method is used to construct ObjectState objects // Method is used to construct ObjectState objects
get(frame) { get(frame) {
const { const {
prev, prev, next, first, last,
next,
first,
last,
} = this.boundedKeyframes(frame); } = this.boundedKeyframes(frame);
return { return {
@ -838,27 +853,32 @@
})), })),
}; };
this.history.do(HistoryActions.CHANGED_LABEL, () => { this.history.do(
HistoryActions.CHANGED_LABEL,
() => {
this.label = undoLabel; this.label = undoLabel;
this.attributes = undoAttributes.unmutable; this.attributes = undoAttributes.unmutable;
for (const mutable of undoAttributes.mutable) { for (const mutable of undoAttributes.mutable) {
this.shapes[mutable.frame].attributes = mutable.attributes; this.shapes[mutable.frame].attributes = mutable.attributes;
} }
this.updated = Date.now(); this.updated = Date.now();
}, () => { },
() => {
this.label = redoLabel; this.label = redoLabel;
this.attributes = redoAttributes.unmutable; this.attributes = redoAttributes.unmutable;
for (const mutable of redoAttributes.mutable) { for (const mutable of redoAttributes.mutable) {
this.shapes[mutable.frame].attributes = mutable.attributes; this.shapes[mutable.frame].attributes = mutable.attributes;
} }
this.updated = Date.now(); this.updated = Date.now();
}, [this.clientID], frame); },
[this.clientID],
frame,
);
} }
_saveAttributes(attributes, frame) { _saveAttributes(attributes, frame) {
const current = this.get(frame); const current = this.get(frame);
const labelAttributes = this.label.attributes const labelAttributes = this.label.attributes.reduce((accumulator, value) => {
.reduce((accumulator, value) => {
accumulator[value.id] = value; accumulator[value.id] = value;
return accumulator; return accumulator;
}, {}); }, {});
@ -879,7 +899,7 @@
// keyframe, but without this attrID // keyframe, but without this attrID
|| !(attrID in this.shapes[frame].attributes) || !(attrID in this.shapes[frame].attributes)
// keyframe with attrID, but with another value // keyframe with attrID, but with another value
|| (this.shapes[frame].attributes[attrID] !== attributes[attrID]); || this.shapes[frame].attributes[attrID] !== attributes[attrID];
} }
} }
let redoShape; let redoShape;
@ -904,8 +924,7 @@
} }
for (const attrID of Object.keys(attributes)) { for (const attrID of Object.keys(attributes)) {
if (labelAttributes[attrID].mutable if (labelAttributes[attrID].mutable && attributes[attrID] !== current.attributes[attrID]) {
&& attributes[attrID] !== current.attributes[attrID]) {
redoShape.attributes[attrID] = attributes[attrID]; redoShape.attributes[attrID] = attributes[attrID];
} }
} }
@ -915,7 +934,9 @@
this.shapes[frame] = redoShape; this.shapes[frame] = redoShape;
} }
this.history.do(HistoryActions.CHANGED_ATTRIBUTES, () => { this.history.do(
HistoryActions.CHANGED_ATTRIBUTES,
() => {
this.attributes = undoAttributes; this.attributes = undoAttributes;
if (undoShape) { if (undoShape) {
this.shapes[frame] = undoShape; this.shapes[frame] = undoShape;
@ -923,17 +944,23 @@
delete this.shapes[frame]; delete this.shapes[frame];
} }
this.updated = Date.now(); this.updated = Date.now();
}, () => { },
() => {
this.attributes = redoAttributes; this.attributes = redoAttributes;
if (redoShape) { if (redoShape) {
this.shapes[frame] = redoShape; this.shapes[frame] = redoShape;
} }
this.updated = Date.now(); this.updated = Date.now();
}, [this.clientID], frame); },
[this.clientID],
frame,
);
} }
_appendShapeActionToHistory(actionType, frame, undoShape, redoShape, undoSource, redoSource) { _appendShapeActionToHistory(actionType, frame, undoShape, redoShape, undoSource, redoSource) {
this.history.do(actionType, () => { this.history.do(
actionType,
() => {
if (!undoShape) { if (!undoShape) {
delete this.shapes[frame]; delete this.shapes[frame];
} else { } else {
@ -941,7 +968,8 @@
} }
this.source = undoSource; this.source = undoSource;
this.updated = Date.now(); this.updated = Date.now();
}, () => { },
() => {
if (!redoShape) { if (!redoShape) {
delete this.shapes[frame]; delete this.shapes[frame];
} else { } else {
@ -949,7 +977,10 @@
} }
this.source = redoSource; this.source = redoSource;
this.updated = Date.now(); this.updated = Date.now();
}, [this.clientID], frame); },
[this.clientID],
frame,
);
} }
_savePoints(points, frame) { _savePoints(points, frame) {
@ -958,7 +989,9 @@
const undoSource = this.source; const undoSource = this.source;
const redoSource = Source.MANUAL; const redoSource = Source.MANUAL;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined; const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = wasKeyframe ? { ...this.shapes[frame], points } : { const redoShape = wasKeyframe
? { ...this.shapes[frame], points }
: {
frame, frame,
points, points,
zOrder: current.zOrder, zOrder: current.zOrder,
@ -985,7 +1018,9 @@
const undoSource = this.source; const undoSource = this.source;
const redoSource = Source.MANUAL; const redoSource = Source.MANUAL;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined; const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = wasKeyframe ? { ...this.shapes[frame], outside } : { const redoShape = wasKeyframe
? { ...this.shapes[frame], outside }
: {
frame, frame,
outside, outside,
zOrder: current.zOrder, zOrder: current.zOrder,
@ -1012,7 +1047,9 @@
const undoSource = this.source; const undoSource = this.source;
const redoSource = Source.MANUAL; const redoSource = Source.MANUAL;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined; const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = wasKeyframe ? { ...this.shapes[frame], occluded } : { const redoShape = wasKeyframe
? { ...this.shapes[frame], occluded }
: {
frame, frame,
occluded, occluded,
zOrder: current.zOrder, zOrder: current.zOrder,
@ -1039,7 +1076,9 @@
const undoSource = this.source; const undoSource = this.source;
const redoSource = Source.MANUAL; const redoSource = Source.MANUAL;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined; const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = wasKeyframe ? { ...this.shapes[frame], zOrder } : { const redoShape = wasKeyframe
? { ...this.shapes[frame], zOrder }
: {
frame, frame,
zOrder, zOrder,
occluded: current.occluded, occluded: current.occluded,
@ -1064,15 +1103,15 @@
const current = this.get(frame); const current = this.get(frame);
const wasKeyframe = frame in this.shapes; const wasKeyframe = frame in this.shapes;
if ((keyframe && wasKeyframe) if ((keyframe && wasKeyframe) || (!keyframe && !wasKeyframe)) {
|| (!keyframe && !wasKeyframe)) {
return; return;
} }
const undoSource = this.source; const undoSource = this.source;
const redoSource = Source.MANUAL; const redoSource = Source.MANUAL;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined; const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = keyframe ? { const redoShape = keyframe
? {
frame, frame,
zOrder: current.zOrder, zOrder: current.zOrder,
points: current.points, points: current.points,
@ -1080,7 +1119,8 @@
occluded: current.occluded, occluded: current.occluded,
attributes: {}, attributes: {},
source: current.source, source: current.source,
} : undefined; }
: undefined;
this.source = Source.MANUAL; this.source = Source.MANUAL;
if (redoShape) { if (redoShape) {
@ -1228,9 +1268,7 @@
// Method is used to construct ObjectState objects // Method is used to construct ObjectState objects
get(frame) { get(frame) {
if (frame !== this.frame) { if (frame !== this.frame) {
throw new ScriptingError( throw new ScriptingError('Got frame is not equal to the frame of the shape');
'Got frame is not equal to the frame of the shape',
);
} }
return { return {
@ -1250,9 +1288,7 @@
save(frame, data) { save(frame, data) {
if (frame !== this.frame) { if (frame !== this.frame) {
throw new ScriptingError( throw new ScriptingError('Got frame is not equal to the frame of the tag');
'Got frame is not equal to the frame of the tag',
);
} }
if (this.lock && data.lock) { if (this.lock && data.lock) {
@ -1322,7 +1358,7 @@
static distance(points, x, y) { static distance(points, x, y) {
function position(x1, y1, x2, y2) { function position(x1, y1, x2, y2) {
return ((x2 - x1) * (y - y1) - (x - x1) * (y2 - y1)); return (x2 - x1) * (y - y1) - (x - x1) * (y2 - y1);
} }
let wn = 0; let wn = 0;
@ -1354,8 +1390,8 @@
// Find the shortest distance from point to an edge // Find the shortest distance from point to an edge
// Get an equation of a line in general // Get an equation of a line in general
const aCoef = (y1 - y2); const aCoef = y1 - y2;
const bCoef = (x2 - x1); const bCoef = x2 - x1;
// Vector (aCoef, bCoef) is a perpendicular to line // Vector (aCoef, bCoef) is a perpendicular to line
// Now find the point where two lines // Now find the point where two lines
@ -1363,13 +1399,9 @@
const xCross = x - aCoef; const xCross = x - aCoef;
const yCross = y - bCoef; const yCross = y - bCoef;
if (((xCross - x1) * (x2 - xCross)) >= 0 if ((xCross - x1) * (x2 - xCross) >= 0 && (yCross - y1) * (y2 - yCross) >= 0) {
&& ((yCross - y1) * (y2 - yCross)) >= 0) {
// Cross point is on segment between p1(x1,y1) and p2(x2,y2) // Cross point is on segment between p1(x1,y1) and p2(x2,y2)
distances.push(Math.sqrt( distances.push(Math.sqrt(Math.pow(x - xCross, 2) + Math.pow(y - yCross, 2)));
Math.pow(x - xCross, 2)
+ Math.pow(y - yCross, 2),
));
} else { } else {
distances.push( distances.push(
Math.min( Math.min(
@ -1407,12 +1439,12 @@
const y2 = points[i + 3]; const y2 = points[i + 3];
// Find the shortest distance from point to an edge // Find the shortest distance from point to an edge
if (((x - x1) * (x2 - x)) >= 0 && ((y - y1) * (y2 - y)) >= 0) { if ((x - x1) * (x2 - x) >= 0 && (y - y1) * (y2 - y) >= 0) {
// Find the length of a perpendicular // Find the length of a perpendicular
// https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line // https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
distances.push( distances.push(
Math.abs((y2 - y1) * x - (x2 - x1) * y + x2 * y1 - y2 * x1) / Math Math.abs((y2 - y1) * x - (x2 - x1) * y + x2 * y1 - y2 * x1)
.sqrt(Math.pow(y2 - y1, 2) + Math.pow(x2 - x1, 2)), / Math.sqrt(Math.pow(y2 - y1, 2) + Math.pow(x2 - x1, 2)),
); );
} else { } else {
// The link below works for lines (which have infinit length) // The link below works for lines (which have infinit length)
@ -1445,9 +1477,7 @@
const x1 = points[i]; const x1 = points[i];
const y1 = points[i + 1]; const y1 = points[i + 1];
distances.push( distances.push(Math.sqrt(Math.pow(x1 - x, 2) + Math.pow(y1 - y, 2)));
Math.sqrt(Math.pow(x1 - x, 2) + Math.pow(y1 - y, 2)),
);
} }
return Math.min.apply(null, distances); return Math.min.apply(null, distances);
@ -1497,11 +1527,12 @@
} }
lowerHull.pop(); lowerHull.pop();
if (upperHull.length if (
=== 1 && lowerHull.length upperHull.length === 1
=== 1 && upperHull[0].x && lowerHull.length === 1
=== lowerHull[0].x && upperHull[0].y && upperHull[0].x === lowerHull[0].x
=== lowerHull[0].y) return upperHull; && upperHull[0].y === lowerHull[0].y
) return upperHull;
return upperHull.concat(lowerHull); return upperHull.concat(lowerHull);
} }
@ -1520,7 +1551,7 @@
static contain(points, x, y) { static contain(points, x, y) {
function isLeft(P0, P1, P2) { function isLeft(P0, P1, P2) {
return ((P1.x - P0.x) * (P2.y - P0.y) - (P2.x - P0.x) * (P1.y - P0.y)); return (P1.x - P0.x) * (P2.y - P0.y) - (P2.x - P0.x) * (P1.y - P0.y);
} }
points = CuboidShape.makeHull(points); points = CuboidShape.makeHull(points);
let wn = 0; let wn = 0;
@ -1559,15 +1590,14 @@
const p2 = points[i + 1] || points[0]; const p2 = points[i + 1] || points[0];
// perpendicular from point to straight length // perpendicular from point to straight length
const distance = (Math.abs((p2.y - p1.y) * x const distance = Math.abs((p2.y - p1.y) * x - (p2.x - p1.x) * y + p2.x * p1.y - p2.y * p1.x)
- (p2.x - p1.x) * y + p2.x * p1.y - p2.y * p1.x))
/ Math.sqrt(Math.pow(p2.y - p1.y, 2) + Math.pow(p2.x - p1.x, 2)); / Math.sqrt(Math.pow(p2.y - p1.y, 2) + Math.pow(p2.x - p1.x, 2));
// check if perpendicular belongs to the straight segment // check if perpendicular belongs to the straight segment
const a = Math.pow(p1.x - x, 2) + Math.pow(p1.y - y, 2); const a = Math.pow(p1.x - x, 2) + Math.pow(p1.y - y, 2);
const b = Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2); const b = Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2);
const c = Math.pow(p2.x - x, 2) + Math.pow(p2.y - y, 2); const c = Math.pow(p2.x - x, 2) + Math.pow(p2.y - y, 2);
if (distance < minDistance && (a + b - c) >= 0 && (c + b - a) >= 0) { if (distance < minDistance && a + b - c >= 0 && c + b - a >= 0) {
minDistance = distance; minDistance = distance;
} }
} }
@ -1586,14 +1616,10 @@
} }
interpolatePosition(leftPosition, rightPosition, offset) { interpolatePosition(leftPosition, rightPosition, offset) {
const positionOffset = leftPosition.points.map((point, index) => ( const positionOffset = leftPosition.points.map((point, index) => rightPosition.points[index] - point);
rightPosition.points[index] - point
));
return { return {
points: leftPosition.points.map((point, index) => ( points: leftPosition.points.map((point, index) => point + positionOffset[index] * offset),
point + positionOffset[index] * offset
)),
occluded: leftPosition.occluded, occluded: leftPosition.occluded,
outside: leftPosition.outside, outside: leftPosition.outside,
zOrder: leftPosition.zOrder, zOrder: leftPosition.zOrder,
@ -1681,7 +1707,8 @@
function matchRightLeft(leftCurve, rightCurve, leftRightMatching) { function matchRightLeft(leftCurve, rightCurve, leftRightMatching) {
const matchedRightPoints = Object.values(leftRightMatching).flat(); const matchedRightPoints = Object.values(leftRightMatching).flat();
const unmatchedRightPoints = rightCurve.map((_, index) => index) const unmatchedRightPoints = rightCurve
.map((_, index) => index)
.filter((index) => !matchedRightPoints.includes(index)); .filter((index) => !matchedRightPoints.includes(index));
const updatedMatching = { ...leftRightMatching }; const updatedMatching = { ...leftRightMatching };
@ -1691,8 +1718,7 @@
} }
for (const key of Object.keys(updatedMatching)) { for (const key of Object.keys(updatedMatching)) {
const sortedRightIndexes = updatedMatching[key] const sortedRightIndexes = updatedMatching[key].sort((a, b) => a - b);
.sort((a, b) => a - b);
updatedMatching[key] = sortedRightIndexes; updatedMatching[key] = sortedRightIndexes;
} }
@ -1715,9 +1741,7 @@
} }
function computeDistance(point1, point2) { function computeDistance(point1, point2) {
return Math.sqrt( return Math.sqrt((point1.x - point2.x) ** 2 + (point1.y - point2.y) ** 2);
((point1.x - point2.x) ** 2) + ((point1.y - point2.y) ** 2),
);
} }
function minimizeSegment(baseLength, N, startInterpolated, stopInterpolated) { function minimizeSegment(baseLength, N, startInterpolated, stopInterpolated) {
@ -1725,9 +1749,7 @@
const minimized = [interpolatedPoints[startInterpolated]]; const minimized = [interpolatedPoints[startInterpolated]];
let latestPushed = startInterpolated; let latestPushed = startInterpolated;
for (let i = startInterpolated + 1; i < stopInterpolated; i++) { for (let i = startInterpolated + 1; i < stopInterpolated; i++) {
const distance = computeDistance( const distance = computeDistance(interpolatedPoints[latestPushed], interpolatedPoints[i]);
interpolatedPoints[latestPushed], interpolatedPoints[i],
);
if (distance >= threshold) { if (distance >= threshold) {
minimized.push(interpolatedPoints[i]); minimized.push(interpolatedPoints[i]);
@ -1771,9 +1793,7 @@
const baseLength = curveLength(leftPoints.slice(start, stop + 1)); const baseLength = curveLength(leftPoints.slice(start, stop + 1));
const N = stop - start + 1; const N = stop - start + 1;
reduced.push( reduced.push(...minimizeSegment(baseLength, N, startInterpolated, stopInterpolated));
...minimizeSegment(baseLength, N, startInterpolated, stopInterpolated),
);
} }
function rightSegment(leftPoint) { function rightSegment(leftPoint) {
@ -1784,9 +1804,7 @@
const baseLength = curveLength(rightPoints.slice(start, stop + 1)); const baseLength = curveLength(rightPoints.slice(start, stop + 1));
const N = stop - start + 1; const N = stop - start + 1;
reduced.push( reduced.push(...minimizeSegment(baseLength, N, startInterpolated, stopInterpolated));
...minimizeSegment(baseLength, N, startInterpolated, stopInterpolated),
);
} }
let previousOpened = null; let previousOpened = null;
@ -1842,12 +1860,11 @@
const rightOffsetVec = curveToOffsetVec(rightPoints, curveLength(rightPoints)); const rightOffsetVec = curveToOffsetVec(rightPoints, curveLength(rightPoints));
const matching = matchLeftRight(leftOffsetVec, rightOffsetVec); const matching = matchLeftRight(leftOffsetVec, rightOffsetVec);
const completedMatching = matchRightLeft( const completedMatching = matchRightLeft(leftOffsetVec, rightOffsetVec, matching);
leftOffsetVec, rightOffsetVec, matching,
);
const interpolatedPoints = Object.keys(completedMatching) const interpolatedPoints = Object.keys(completedMatching)
.map((leftPointIdx) => +leftPointIdx).sort((a, b) => a - b) .map((leftPointIdx) => +leftPointIdx)
.sort((a, b) => a - b)
.reduce((acc, leftPointIdx) => { .reduce((acc, leftPointIdx) => {
const leftPoint = leftPoints[leftPointIdx]; const leftPoint = leftPoints[leftPointIdx];
for (const rightPointIdx of completedMatching[leftPointIdx]) { for (const rightPointIdx of completedMatching[leftPointIdx]) {
@ -1861,12 +1878,7 @@
return acc; return acc;
}, []); }, []);
const reducedPoints = reduceInterpolation( const reducedPoints = reduceInterpolation(interpolatedPoints, completedMatching, leftPoints, rightPoints);
interpolatedPoints,
completedMatching,
leftPoints,
rightPoints,
);
return { return {
points: toArray(reducedPoints), points: toArray(reducedPoints),
@ -1897,8 +1909,7 @@
points: [...rightPosition.points, rightPosition.points[0], rightPosition.points[1]], points: [...rightPosition.points, rightPosition.points[0], rightPosition.points[1]],
}; };
const result = PolyTrack.prototype.interpolatePosition const result = PolyTrack.prototype.interpolatePosition.call(this, copyLeft, copyRight, offset);
.call(this, copyLeft, copyRight, offset);
return { return {
...result, ...result,
@ -1959,14 +1970,10 @@
} }
interpolatePosition(leftPosition, rightPosition, offset) { interpolatePosition(leftPosition, rightPosition, offset) {
const positionOffset = leftPosition.points.map((point, index) => ( const positionOffset = leftPosition.points.map((point, index) => rightPosition.points[index] - point);
rightPosition.points[index] - point
));
return { return {
points: leftPosition.points.map((point, index) => ( points: leftPosition.points.map((point, index) => point + positionOffset[index] * offset),
point + positionOffset[index] * offset
)),
occluded: leftPosition.occluded, occluded: leftPosition.occluded,
outside: leftPosition.outside, outside: leftPosition.outside,
zOrder: leftPosition.zOrder, zOrder: leftPosition.zOrder,

@ -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;
} }

@ -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
*/
/** /**
* External API which should be used by for development * External API which should be used by for development
@ -18,31 +13,18 @@ function build() {
const Log = require('./log'); const Log = require('./log');
const ObjectState = require('./object-state'); const ObjectState = require('./object-state');
const Statistics = require('./statistics'); const Statistics = require('./statistics');
const Comment = require('./comment');
const Issue = require('./issue');
const Review = require('./review');
const { Job, Task } = require('./session'); const { Job, Task } = require('./session');
const { Project } = require('./project');
const { Attribute, Label } = require('./labels'); const { Attribute, Label } = require('./labels');
const MLModel = require('./ml-model'); const MLModel = require('./ml-model');
const { const enums = require('./enums');
ShareFileType,
TaskStatus,
TaskMode,
AttributeType,
ObjectType,
ObjectShape,
LogType,
HistoryActions,
RQStatus,
colors,
Source,
} = require('./enums');
const { const {
Exception, Exception, ArgumentError, DataError, ScriptingError, PluginError, ServerError,
ArgumentError,
DataError,
ScriptingError,
PluginError,
ServerError,
} = require('./exceptions'); } = require('./exceptions');
const User = require('./user'); const User = require('./user');
@ -80,8 +62,7 @@ function build() {
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
*/ */
async about() { async about() {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper(cvat.server.about);
.apiWrapper(cvat.server.about);
return result; return result;
}, },
/** /**
@ -103,8 +84,7 @@ function build() {
* @throws {module:API.cvat.exceptions.ServerError} * @throws {module:API.cvat.exceptions.ServerError}
*/ */
async share(directory = '/') { async share(directory = '/') {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper(cvat.server.share, directory);
.apiWrapper(cvat.server.share, directory);
return result; return result;
}, },
/** /**
@ -117,8 +97,7 @@ function build() {
* @throws {module:API.cvat.exceptions.ServerError} * @throws {module:API.cvat.exceptions.ServerError}
*/ */
async formats() { async formats() {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper(cvat.server.formats);
.apiWrapper(cvat.server.formats);
return result; return result;
}, },
/** /**
@ -131,12 +110,10 @@ function build() {
* @throws {module:API.cvat.exceptions.ServerError} * @throws {module:API.cvat.exceptions.ServerError}
*/ */
async userAgreements() { async userAgreements() {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper(cvat.server.userAgreements);
.apiWrapper(cvat.server.userAgreements);
return result; return result;
}, },
/** /**
* Method allows to register on a server * Method allows to register on a server
* @method register * @method register
* @async * @async
@ -152,7 +129,9 @@ function build() {
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError} * @throws {module:API.cvat.exceptions.ServerError}
*/ */
async register( async register(username, firstName, lastName, email, password1, password2, userConfirmations) {
const result = await PluginRegistry.apiWrapper(
cvat.server.register,
username, username,
firstName, firstName,
lastName, lastName,
@ -160,10 +139,7 @@ function build() {
password1, password1,
password2, password2,
userConfirmations, userConfirmations,
) { );
const result = await PluginRegistry
.apiWrapper(cvat.server.register, username, firstName,
lastName, email, password1, password2, userConfirmations);
return result; return result;
}, },
/** /**
@ -177,8 +153,7 @@ function build() {
* @throws {module:API.cvat.exceptions.ServerError} * @throws {module:API.cvat.exceptions.ServerError}
*/ */
async login(username, password) { async login(username, password) {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper(cvat.server.login, username, password);
.apiWrapper(cvat.server.login, username, password);
return result; return result;
}, },
/** /**
@ -190,8 +165,7 @@ function build() {
* @throws {module:API.cvat.exceptions.ServerError} * @throws {module:API.cvat.exceptions.ServerError}
*/ */
async logout() { async logout() {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper(cvat.server.logout);
.apiWrapper(cvat.server.logout);
return result; return result;
}, },
/** /**
@ -199,12 +173,54 @@ function build() {
* @method changePassword * @method changePassword
* @async * @async
* @memberof module:API.cvat.server * @memberof module:API.cvat.server
* @param {string} oldPassword Current password for the account
* @param {string} newPassword1 New password for the account
* @param {string} newPassword2 Confirmation password for the account
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError} * @throws {module:API.cvat.exceptions.ServerError}
*/ */
async changePassword(oldPassword, newPassword1, newPassword2) { async changePassword(oldPassword, newPassword1, newPassword2) {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper(
.apiWrapper(cvat.server.changePassword, oldPassword, newPassword1, newPassword2); cvat.server.changePassword,
oldPassword,
newPassword1,
newPassword2,
);
return result;
},
/**
* Method allows to reset user password
* @method requestPasswordReset
* @async
* @memberof module:API.cvat.server
* @param {string} email A email address for the account
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async requestPasswordReset(email) {
const result = await PluginRegistry.apiWrapper(cvat.server.requestPasswordReset, email);
return result;
},
/**
* Method allows to confirm reset user password
* @method resetPassword
* @async
* @memberof module:API.cvat.server
* @param {string} newPassword1 New password for the account
* @param {string} newPassword2 Confirmation password for the account
* @param {string} uid User id
* @param {string} token Request authentication token
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async resetPassword(newPassword1, newPassword2, uid, token) {
const result = await PluginRegistry.apiWrapper(
cvat.server.resetPassword,
newPassword1,
newPassword2,
uid,
token,
);
return result; return result;
}, },
/** /**
@ -217,8 +233,7 @@ function build() {
* @throws {module:API.cvat.exceptions.ServerError} * @throws {module:API.cvat.exceptions.ServerError}
*/ */
async authorized() { async authorized() {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper(cvat.server.authorized);
.apiWrapper(cvat.server.authorized);
return result; return result;
}, },
/** /**
@ -233,8 +248,75 @@ function build() {
* @throws {module:API.cvat.exceptions.ServerError} * @throws {module:API.cvat.exceptions.ServerError}
*/ */
async request(url, data) { async request(url, data) {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper(cvat.server.request, url, data);
.apiWrapper(cvat.server.request, url, data); return result;
},
/**
* Method returns apps that are installed on the server
* @method installedApps
* @async
* @memberof module:API.cvat.server
* @returns {Object} map {installedApp: boolean}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async installedApps() {
const result = await PluginRegistry.apiWrapper(cvat.server.installedApps);
return result;
},
},
/**
* Namespace is used for getting projects
* @namespace projects
* @memberof module:API.cvat
*/
projects: {
/**
* @typedef {Object} ProjectFilter
* @property {string} name Check if name contains this value
* @property {module:API.cvat.enums.ProjectStatus} status
* Check if status contains this value
* @property {integer} id Check if id equals this value
* @property {integer} page Get specific page
* (default REST API returns 20 projects per request.
* In order to get more, it is need to specify next page)
* @property {string} owner Check if owner user contains this value
* @property {string} search Combined search of contains among all fields
* @global
*/
/**
* Method returns list of projects corresponding to a filter
* @method get
* @async
* @memberof module:API.cvat.projects
* @param {ProjectFilter} [filter={}] project filter
* @returns {module:API.cvat.classes.Project[]}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async get(filter = {}) {
const result = await PluginRegistry.apiWrapper(cvat.projects.get, filter);
return result;
},
/**
* Method returns list of project names with project ids
* corresponding to a search phrase
* used for autocomplete field
* @method searchNames
* @async
* @memberof module:API.cvat.projects
* @param {string} [search = ''] search phrase
* @param {number} [limit = 10] number of returning project names
* @returns {module:API.cvat.classes.Project[]}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*
*/
async searchNames(search = '', limit = 10) {
const result = await PluginRegistry.apiWrapper(cvat.projects.searchNames, search, limit);
return result; return result;
}, },
}, },
@ -255,6 +337,7 @@ function build() {
* @property {integer} page Get specific page * @property {integer} page Get specific page
* (default REST API returns 20 tasks per request. * (default REST API returns 20 tasks per request.
* In order to get more, it is need to specify next page) * In order to get more, it is need to specify next page)
* @property {integer} projectId Check if project_id field contains this value
* @property {string} owner Check if owner user contains this value * @property {string} owner Check if owner user contains this value
* @property {string} assignee Check if assigneed contains this value * @property {string} assignee Check if assigneed contains this value
* @property {string} search Combined search of contains among all fields * @property {string} search Combined search of contains among all fields
@ -272,8 +355,7 @@ function build() {
* @throws {module:API.cvat.exceptions.ServerError} * @throws {module:API.cvat.exceptions.ServerError}
*/ */
async get(filter = {}) { async get(filter = {}) {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper(cvat.tasks.get, filter);
.apiWrapper(cvat.tasks.get, filter);
return result; return result;
}, },
}, },
@ -302,8 +384,7 @@ function build() {
* @throws {module:API.cvat.exceptions.ServerError} * @throws {module:API.cvat.exceptions.ServerError}
*/ */
async get(filter = {}) { async get(filter = {}) {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper(cvat.jobs.get, filter);
.apiWrapper(cvat.jobs.get, filter);
return result; return result;
}, },
}, },
@ -330,8 +411,7 @@ function build() {
* @throws {module:API.cvat.exceptions.ServerError} * @throws {module:API.cvat.exceptions.ServerError}
*/ */
async get(filter = {}) { async get(filter = {}) {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper(cvat.users.get, filter);
.apiWrapper(cvat.users.get, filter);
return result; return result;
}, },
}, },
@ -430,8 +510,7 @@ function build() {
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
*/ */
async list() { async list() {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper(cvat.plugins.list);
.apiWrapper(cvat.plugins.list);
return result; return result;
}, },
/** /**
@ -443,11 +522,11 @@ function build() {
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
*/ */
async register(plugin) { async register(plugin) {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper(cvat.plugins.register, plugin);
.apiWrapper(cvat.plugins.register, plugin);
return result; return result;
}, },
}, },
/** /**
* Namespace is used for serverless functions management (mainly related with DL models) * Namespace is used for serverless functions management (mainly related with DL models)
* @namespace lambda * @namespace lambda
@ -464,8 +543,7 @@ function build() {
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
*/ */
async list() { async list() {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper(cvat.lambda.list);
.apiWrapper(cvat.lambda.list);
return result; return result;
}, },
@ -483,8 +561,7 @@ function build() {
* @throws {module:API.cvat.exceptions.ArgumentError} * @throws {module:API.cvat.exceptions.ArgumentError}
*/ */
async run(task, model, args) { async run(task, model, args) {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper(cvat.lambda.run, task, model, args);
.apiWrapper(cvat.lambda.run, task, model, args);
return result; return result;
}, },
@ -502,8 +579,7 @@ function build() {
* @throws {module:API.cvat.exceptions.ArgumentError} * @throws {module:API.cvat.exceptions.ArgumentError}
*/ */
async call(task, model, args) { async call(task, model, args) {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper(cvat.lambda.call, task, model, args);
.apiWrapper(cvat.lambda.call, task, model, args);
return result; return result;
}, },
@ -518,8 +594,7 @@ function build() {
* @throws {module:API.cvat.exceptions.ArgumentError} * @throws {module:API.cvat.exceptions.ArgumentError}
*/ */
async cancel(requestID) { async cancel(requestID) {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper(cvat.lambda.cancel, requestID);
.apiWrapper(cvat.lambda.cancel, requestID);
return result; return result;
}, },
@ -542,8 +617,7 @@ function build() {
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
*/ */
async listen(requestID, onChange) { async listen(requestID, onChange) {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper(cvat.lambda.listen, requestID, onChange);
.apiWrapper(cvat.lambda.listen, requestID, onChange);
return result; return result;
}, },
@ -556,8 +630,7 @@ function build() {
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
*/ */
async requests() { async requests() {
const result = await PluginRegistry const result = await PluginRegistry.apiWrapper(cvat.lambda.requests);
.apiWrapper(cvat.lambda.requests);
return result; return result;
}, },
}, },
@ -659,19 +732,7 @@ function build() {
* @namespace enums * @namespace enums
* @memberof module:API.cvat * @memberof module:API.cvat
*/ */
enums: { enums,
ShareFileType,
TaskStatus,
TaskMode,
AttributeType,
ObjectType,
ObjectShape,
LogType,
HistoryActions,
RQStatus,
colors,
Source,
},
/** /**
* Namespace is used for access to exceptions * Namespace is used for access to exceptions
* @namespace exceptions * @namespace exceptions
@ -691,8 +752,9 @@ function build() {
* @memberof module:API.cvat * @memberof module:API.cvat
*/ */
classes: { classes: {
Task,
User, User,
Project,
Task,
Job, Job,
Log, Log,
Attribute, Attribute,
@ -700,10 +762,14 @@ function build() {
Statistics, Statistics,
ObjectState, ObjectState,
MLModel, MLModel,
Comment,
Issue,
Review,
}, },
}; };
cvat.server = Object.freeze(cvat.server); cvat.server = Object.freeze(cvat.server);
cvat.projects = Object.freeze(cvat.projects);
cvat.tasks = Object.freeze(cvat.tasks); cvat.tasks = Object.freeze(cvat.tasks);
cvat.jobs = Object.freeze(cvat.jobs); cvat.jobs = Object.freeze(cvat.jobs);
cvat.users = Object.freeze(cvat.users); cvat.users = Object.freeze(cvat.users);

@ -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,7 +1,6 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019-2020 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
(() => { (() => {
/** /**
@ -34,6 +33,22 @@
COMPLETED: 'completed', COMPLETED: 'completed',
}); });
/**
* Review statuses
* @enum {string}
* @name ReviewStatus
* @memberof module:API.cvat.enums
* @property {string} ACCEPTED 'accepted'
* @property {string} REJECTED 'rejected'
* @property {string} REVIEW_FURTHER 'review_further'
* @readonly
*/
const ReviewStatus = Object.freeze({
ACCEPTED: 'accepted',
REJECTED: 'rejected',
REVIEW_FURTHER: 'review_further',
});
/** /**
* List of RQ statuses * List of RQ statuses
* @enum {string} * @enum {string}
@ -272,16 +287,42 @@
* @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');
@ -32,15 +27,13 @@
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(
this,
Object.freeze({
system: { system: {
/** /**
* @name system * @name system
@ -141,7 +134,8 @@
*/ */
get: () => column, get: () => column,
}, },
})); }),
);
} }
/** /**
@ -246,7 +240,9 @@
constructor(message, code) { constructor(message, code) {
super(message); super(message);
Object.defineProperties(this, Object.freeze({ Object.defineProperties(
this,
Object.freeze({
/** /**
* @name code * @name code
* @type {(string|integer)} * @type {(string|integer)}
@ -257,7 +253,8 @@
code: { code: {
get: () => 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');
@ -25,16 +19,11 @@
*/ */
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,
Object.freeze({
/** /**
* @name filename * @name filename
* @type {string} * @type {string}
@ -95,7 +84,8 @@
value: decodeForward, value: decodeForward,
writable: false, writable: false,
}, },
})); }),
);
} }
/** /**
@ -111,8 +101,7 @@
* @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,23 +162,28 @@
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)
.then((chunk) => {
frameDataCache[this.tid].activeChunkRequest.completed = true; frameDataCache[this.tid].activeChunkRequest.completed = true;
if (!taskDataCache.nextChunkRequest) { if (!taskDataCache.nextChunkRequest) {
provider.requestDecodeBlock(chunk, provider.requestDecodeBlock(
chunk,
taskDataCache.activeChunkRequest.start, taskDataCache.activeChunkRequest.start,
taskDataCache.activeChunkRequest.stop, taskDataCache.activeChunkRequest.stop,
taskDataCache.activeChunkRequest.onDecodeAll, taskDataCache.activeChunkRequest.onDecodeAll,
taskDataCache.activeChunkRequest.rejectRequestAll); taskDataCache.activeChunkRequest.rejectRequestAll,
);
} }
}).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));
} }
}).finally(() => { })
.finally(() => {
if (taskDataCache.nextChunkRequest) { if (taskDataCache.nextChunkRequest) {
if (taskDataCache.activeChunkRequest) { if (taskDataCache.activeChunkRequest) {
for (const r of taskDataCache.activeChunkRequest.callbacks) { for (const r of taskDataCache.activeChunkRequest.callbacks) {
@ -205,15 +200,19 @@
if (isNode) { if (isNode) {
resolve('Dummy data'); resolve('Dummy data');
} else if (isBrowser) { } else if (isBrowser) {
provider.frame(this.number).then((frame) => { provider
.frame(this.number)
.then((frame) => {
if (frame === null) { if (frame === null) {
onServerRequest(); onServerRequest();
const activeRequest = frameDataCache[this.tid].activeChunkRequest; const activeRequest = frameDataCache[this.tid].activeChunkRequest;
if (!provider.isChunkCached(start, stop)) { if (!provider.isChunkCached(start, stop)) {
if (!activeRequest if (
!activeRequest
|| (activeRequest || (activeRequest
&& activeRequest.completed && activeRequest.completed
&& activeRequest.chunkNumber !== chunkNumber)) { && activeRequest.chunkNumber !== chunkNumber)
) {
if (activeRequest && activeRequest.rejectRequestAll) { if (activeRequest && activeRequest.rejectRequestAll) {
activeRequest.rejectRequestAll(); activeRequest.rejectRequestAll();
} }
@ -225,16 +224,17 @@
onDecodeAll, onDecodeAll,
rejectRequestAll, rejectRequestAll,
completed: false, completed: false,
callbacks: [{ callbacks: [
{
resolve: resolveWrapper, resolve: resolveWrapper,
reject, reject,
frameNumber: this.number, frameNumber: this.number,
}], },
],
}; };
makeActiveRequest(); makeActiveRequest();
} else if (activeRequest.chunkNumber === chunkNumber) { } else if (activeRequest.chunkNumber === chunkNumber) {
if (!activeRequest.onDecodeAll if (!activeRequest.onDecodeAll && !activeRequest.rejectRequestAll) {
&& !activeRequest.rejectRequestAll) {
activeRequest.onDecodeAll = onDecodeAll; activeRequest.onDecodeAll = onDecodeAll;
activeRequest.rejectRequestAll = rejectRequestAll; activeRequest.rejectRequestAll = rejectRequestAll;
} }
@ -258,11 +258,13 @@
onDecodeAll, onDecodeAll,
rejectRequestAll, rejectRequestAll,
completed: false, completed: false,
callbacks: [{ callbacks: [
{
resolve: resolveWrapper, resolve: resolveWrapper,
reject, reject,
frameNumber: this.number, frameNumber: this.number,
}], },
],
}; };
} }
} else { } else {
@ -271,14 +273,15 @@
reject, reject,
frameNumber: this.number, frameNumber: this.number,
}); });
provider.requestDecodeBlock(null, start, stop, provider.requestDecodeBlock(null, start, stop, onDecodeAll, rejectRequestAll);
onDecodeAll, rejectRequestAll);
} }
} else { } else {
if (this.number % chunkSize > chunkSize / 4 if (
this.number % chunkSize > chunkSize / 4
&& provider.decodedBlocksCacheSize > 1 && provider.decodedBlocksCacheSize > 1
&& this.decodeForward && this.decodeForward
&& !provider.isNextChunkExists(this.number)) { && !provider.isNextChunkExists(this.number)
) {
const nextChunkNumber = Math.floor(this.number / chunkSize) + 1; const nextChunkNumber = Math.floor(this.number / chunkSize) + 1;
if (nextChunkNumber * chunkSize < this.stopFrame) { if (nextChunkNumber * chunkSize < this.stopFrame) {
provider.setReadyToLoading(nextChunkNumber); provider.setReadyToLoading(nextChunkNumber);
@ -299,14 +302,14 @@
makeActiveRequest(); makeActiveRequest();
} }
} else { } else {
provider.requestDecodeBlock(null, nextStart, nextStop, provider.requestDecodeBlock(null, nextStart, nextStop, null, null);
null, null);
} }
} }
} }
resolveWrapper(frame); resolveWrapper(frame);
} }
}).catch((exception) => { })
.catch((exception) => {
if (exception instanceof Exception) { if (exception instanceof Exception) {
reject(exception); reject(exception);
} else { } else {
@ -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,21 +376,26 @@
decodeForward: false, decodeForward: false,
}); });
frameData.data().then(() => { frameData
if (!(chunkIdx in this._requestedChunks) .data()
|| !this._requestedChunks[chunkIdx].requestedFrames.has(requestedFrame)) { .then(() => {
if (
!(chunkIdx in this._requestedChunks)
|| !this._requestedChunks[chunkIdx].requestedFrames.has(requestedFrame)
) {
reject(chunkIdx); reject(chunkIdx);
} else { } else {
this._requestedChunks[chunkIdx].requestedFrames.delete(requestedFrame); this._requestedChunks[chunkIdx].requestedFrames.delete(requestedFrame);
this._requestedChunks[chunkIdx].buffer[requestedFrame] = frameData; this._requestedChunks[chunkIdx].buffer[requestedFrame] = frameData;
if (this._requestedChunks[chunkIdx].requestedFrames.size === 0) { if (this._requestedChunks[chunkIdx].requestedFrames.size === 0) {
const bufferedframes = Object.keys( const bufferedframes = Object.keys(this._requestedChunks[chunkIdx].buffer).map(
this._requestedChunks[chunkIdx].buffer, (f) => +f,
).map((f) => +f); );
this._requestedChunks[chunkIdx].resolve(new Set(bufferedframes)); 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,8 +538,11 @@
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
.getPreview(taskID)
.then((result) => {
if (isNode) { if (isNode) {
// eslint-disable-next-line no-undef
resolve(global.Buffer.from(result, 'binary').toString('base64')); resolve(global.Buffer.from(result, 'binary').toString('base64'));
} else if (isBrowser) { } else if (isBrowser) {
const reader = new FileReader(); const reader = new FileReader();
@ -540,28 +551,26 @@
}; };
reader.readAsDataURL(result); reader.readAsDataURL(result);
} }
}).catch((error) => { })
.catch((error) => {
reject(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,16 +1,9 @@
/* // 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');
/** /**
@ -42,12 +35,12 @@
} }
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,
Object.freeze({
/** /**
* @name id * @name id
* @type {integer} * @type {integer}
@ -108,7 +101,8 @@
values: { values: {
get: () => [...data.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;
} }
@ -151,14 +145,18 @@
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,
Object.freeze({
/** /**
* @name id * @name id
* @type {integer} * @type {integer}
@ -206,7 +204,8 @@
attributes: { attributes: {
get: () => [...data.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,
framework: model.framework,
labels: [...model.labels],
type: model.kind, 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,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 { 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');
@ -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');
} }
@ -77,8 +73,7 @@ class Log {
* @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);
try {
this.saving = true;
await serverProxy.logs.save(collectionToSend.map((log) => log.dump())); await serverProxy.logs.save(collectionToSend.map((log) => log.dump()));
for (const rule of Object.values(this.ignoreRules)) { for (const rule of Object.values(this.ignoreRules)) {
rule.lastLog = null; rule.lastLog = null;
} }
this.collection = []; this.collection = [];
this.workingTime = 0; this.workingTime = 0;
this.lastLogTime = Date.now(); this.lastLogTime = Date.now();
} finally {
this.saving = false;
}
}; };
module.exports = new LoggerStorage(); module.exports = new LoggerStorage();

@ -1,7 +1,6 @@
/* // 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
@ -15,6 +14,11 @@ 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,
},
};
} }
/** /**
@ -68,6 +72,16 @@ class MLModel {
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,14 +1,9 @@
/* // 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');
@ -77,7 +72,9 @@ const { Source } = require('./enums');
writable: false, writable: false,
}); });
Object.defineProperties(this, Object.freeze({ Object.defineProperties(
this,
Object.freeze({
// Internal property. We don't need document it. // Internal property. We don't need document it.
updateFlags: { updateFlags: {
get: () => data.updateFlags, get: () => data.updateFlags,
@ -197,8 +194,9 @@ const { Source } = require('./enums');
} else { } else {
throw new ArgumentError( throw new ArgumentError(
'Points are expected to be an array ' 'Points are expected to be an array '
+ `but got ${typeof (points) === 'object' + `but got ${
? points.constructor.name : typeof (points)}`, typeof points === 'object' ? points.constructor.name : typeof points
}`,
); );
} }
}, },
@ -263,7 +261,7 @@ const { Source } = require('./enums');
* @instance * @instance
*/ */
get: () => { get: () => {
if (typeof (data.keyframes) === 'object') { if (typeof data.keyframes === 'object') {
return { ...data.keyframes }; return { ...data.keyframes };
} }
@ -304,7 +302,7 @@ const { Source } = require('./enums');
* @instance * @instance
*/ */
get: () => { get: () => {
if (typeof (data.pinned) === 'boolean') { if (typeof data.pinned === 'boolean') {
return data.pinned; return data.pinned;
} }
@ -338,11 +336,14 @@ const { Source } = require('./enums');
*/ */
get: () => data.attributes, get: () => data.attributes,
set: (attributes) => { set: (attributes) => {
if (typeof (attributes) !== 'object') { if (typeof attributes !== 'object') {
throw new ArgumentError( throw new ArgumentError(
'Attributes are expected to be an object ' 'Attributes are expected to be an object '
+ `but got ${typeof (attributes) === 'object' + `but got ${
? attributes.constructor.name : typeof (attributes)}`, typeof attributes === 'object'
? attributes.constructor.name
: typeof attributes
}`,
); );
} }
@ -352,7 +353,8 @@ const { Source } = require('./enums');
} }
}, },
}, },
})); }),
);
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;
} }
@ -403,8 +405,7 @@ const { Source } = require('./enums');
* @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;
} }
@ -422,8 +423,7 @@ const { Source } = require('./enums');
* @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(
this,
Object.freeze({
get: { get: {
value: get, value: get,
writable: false, 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`,
authenticationData, {
proxy: config.proxy, proxy: config.proxy,
}, });
);
} catch (errorData) { } catch (errorData) {
throw generateError(errorData); throw generateError(errorData);
} }
@ -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 (
await Axios({
url, url,
...data, ...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,8 +447,7 @@
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) {
@ -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(
new ServerError(
`Unknown task state has been received: ${response.data.state}`, `Unknown task state has been received: ${response.data.state}`,
500, 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,11 +572,27 @@
return response.data; return response.data;
} }
async function saveJob(id, jobData) { async function getJobReviews(jobID) {
const { backendAPI } = config; const { backendAPI } = config;
let response = null;
try { try {
await Axios.patch(`${backendAPI}/jobs/${id}`, JSON.stringify(jobData), { 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, proxy: config.proxy,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -475,22 +601,84 @@
} catch (errorData) { } catch (errorData) {
throw generateError(errorData); throw generateError(errorData);
} }
return response.data;
} }
async function getUsers(id = null) { async function getJobIssues(jobID) {
const { backendAPI } = config; const { backendAPI } = config;
let response = null; let response = null;
try { try {
if (id === null) { response = await Axios.get(`${backendAPI}/jobs/${jobID}/issues`, {
response = await Axios.get(`${backendAPI}/users?page_size=all`, {
proxy: config.proxy, proxy: config.proxy,
}); });
} else { } catch (errorData) {
response = await Axios.get(`${backendAPI}/users/${id}`, { 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) {
const { backendAPI } = config;
try {
await Axios.patch(`${backendAPI}/jobs/${id}`, JSON.stringify(jobData), {
proxy: config.proxy, proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
}); });
} catch (errorData) {
throw generateError(errorData);
} }
}
async function getUsers(filter = 'page_size=all') {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/users?${filter}`, {
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,7 +850,8 @@
async function request() { async function request() {
Axios.get(`${url}`, { Axios.get(`${url}`, {
proxy: config.proxy, proxy: config.proxy,
}).then((response) => { })
.then((response) => {
if (response.status === 202) { if (response.status === 202) {
setTimeout(request, 3000); setTimeout(request, 3000);
} else { } else {
@ -663,7 +859,8 @@
url = `${baseURL}?${query}`; url = `${baseURL}?${query}`;
resolve(url); resolve(url);
} }
}).catch((errorData) => { })
.catch((errorData) => {
reject(generateError(errorData)); reject(generateError(errorData));
}); });
} }
@ -704,8 +901,7 @@
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',
@ -722,8 +918,7 @@
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',
@ -767,17 +962,29 @@
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) { } catch (errorData) {
throw generateError(errorData); throw generateError(errorData);
} }
} }
Object.defineProperties(this, Object.freeze({ async function installedApps() {
const { backendAPI } = config;
try {
const response = await Axios.get(`${backendAPI}/server/plugins`, {
proxy: config.proxy,
});
return response.data;
} catch (errorData) {
throw generateError(errorData);
}
}
Object.defineProperties(
this,
Object.freeze({
server: { server: {
value: Object.freeze({ value: Object.freeze({
about, about,
@ -787,10 +994,24 @@
login, login,
logout, logout,
changePassword, changePassword,
requestPasswordReset,
resetPassword,
authorized, authorized,
register, register,
request: serverRequest, request: serverRequest,
userAgreements, userAgreements,
installedApps,
}),
writable: false,
},
projects: {
value: Object.freeze({
get: getProjects,
searchNames: searchProjectNames,
save: saveProject,
create: createProject,
delete: deleteProject,
}), }),
writable: false, writable: false,
}, },
@ -808,16 +1029,21 @@
jobs: { jobs: {
value: Object.freeze({ value: Object.freeze({
getJob, get: getJob,
saveJob, save: saveJob,
issues: getJobIssues,
reviews: {
get: getJobReviews,
create: createReview,
},
}), }),
writable: false, writable: false,
}, },
users: { users: {
value: Object.freeze({ value: Object.freeze({
getUsers, get: getUsers,
getSelf, self: getSelf,
}), }),
writable: false, writable: false,
}, },
@ -859,7 +1085,22 @@
}), }),
writable: false, 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,8 +1,6 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
(() => { (() => {
/** /**
@ -12,7 +10,9 @@
*/ */
class Statistics { class Statistics {
constructor(label, total) { constructor(label, total) {
Object.defineProperties(this, Object.freeze({ Object.defineProperties(
this,
Object.freeze({
/** /**
* Statistics by labels with a structure: * Statistics by labels with a structure:
* @example * @example
@ -91,7 +91,8 @@
total: { total: {
get: () => JSON.parse(JSON.stringify(total)), get: () => JSON.parse(JSON.stringify(total)),
}, },
})); }),
);
} }
} }

@ -1,7 +1,6 @@
/* // Copyright (C) 2019-2020 Intel Corporation
* Copyright (C) 2019 Intel Corporation //
* SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
*/
(() => { (() => {
/** /**
@ -27,13 +26,14 @@
}; };
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(
this,
Object.freeze({
id: { id: {
/** /**
* @name id * @name id
@ -154,7 +154,29 @@
*/ */
get: () => !data.email_verification_required, 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(
window.cvat.jobs.get({
taskID: 1, taskID: 1,
jobID: 1, jobID: 1,
})).rejects.toThrow(window.cvat.exceptions.ArgumentError); }),
).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(
window.cvat.jobs.get({
jobID: '1', jobID: '1',
})).rejects.toThrow(window.cvat.exceptions.ArgumentError); }),
).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(
window.cvat.jobs.get({
taskID: '1', taskID: '1',
})).rejects.toThrow(window.cvat.exceptions.ArgumentError); }),
).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(
window.cvat.jobs.get({
unknown: 50, unknown: 50,
})).rejects.toThrow(window.cvat.exceptions.ArgumentError); }),
).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(
window.cvat.tasks.get({
id: '50', id: '50',
})).rejects.toThrow(window.cvat.exceptions.ArgumentError); }),
).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(
window.cvat.tasks.get({
unknown: '5', unknown: '5',
})).rejects.toThrow(window.cvat.exceptions.ArgumentError); }),
).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', default_value: 'false',
input_type: 'checkbox', input_type: 'checkbox',
mutable: true, mutable: true,
name: 'parked', name: 'parked',
values: ['false'], 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",
attributes: [
{
default_value: 'false', default_value: 'false',
input_type: 'checkbox', input_type: 'checkbox',
mutable: true, mutable: true,
name: 'parked', name: 'parked',
values: ['false'], 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(
window.cvat.users.get({
unknown: '50', unknown: '50',
})).rejects.toThrow(window.cvat.exceptions.ArgumentError); }),
).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(
window.cvat.users.get({
self: 1, self: 1,
})).rejects.toThrow(window.cvat.exceptions.ArgumentError); }),
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
}); });

@ -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', () => {
@ -25,7 +18,7 @@ describe('Feature: toJSONQuery', () => {
const annotationsFilter = new AnnotationsFilter(); const annotationsFilter = new AnnotationsFilter();
const [groups, query] = annotationsFilter.toJSONQuery([]); const [groups, query] = annotationsFilter.toJSONQuery([]);
expect(Array.isArray(groups)).toBeTruthy(); expect(Array.isArray(groups)).toBeTruthy();
expect(typeof (query)).toBe('string'); expect(typeof query).toBe('string');
}); });
test('convert empty fitlers to a json query', () => { test('convert empty fitlers to a json query', () => {
@ -64,61 +57,65 @@ describe('Feature: toJSONQuery', () => {
test('convert filters to a json query', () => { test('convert filters to a json query', () => {
const annotationsFilter = new AnnotationsFilter(); const annotationsFilter = new AnnotationsFilter();
const [groups, query] = annotationsFilter const [groups, query] = annotationsFilter.toJSONQuery(['clientID==5 & shape=="rectangle" & label==["car"]']);
.toJSONQuery(['clientID==5 & shape=="rectangle" & label==["car"]']); expect(groups).toEqual([['clientID==5', '&', 'shape=="rectangle"', '&', 'label==["car"]']]);
expect(groups).toEqual([
['clientID==5', '&', 'shape=="rectangle"', '&', 'label==["car"]'],
]);
expect(query).toBe('$.objects[?((@.clientID==5&@.shape=="rectangle"&@.label==["car"]))].clientID'); expect(query).toBe('$.objects[?((@.clientID==5&@.shape=="rectangle"&@.label==["car"]))].clientID');
}); });
test('convert filters to a json query', () => { test('convert filters to a json query', () => {
const annotationsFilter = new AnnotationsFilter(); const annotationsFilter = new AnnotationsFilter();
const [groups, query] = annotationsFilter const [groups, query] = annotationsFilter.toJSONQuery(['label=="car" | width >= height & type=="track"']);
.toJSONQuery(['label=="car" | width >= height & type=="track"']); expect(groups).toEqual([['label=="car"', '|', 'width >= height', '&', 'type=="track"']]);
expect(groups).toEqual([
['label=="car"', '|', 'width >= height', '&', 'type=="track"'],
]);
expect(query).toBe('$.objects[?((@.label=="car"|@.width>=@.height&@.type=="track"))].clientID'); expect(query).toBe('$.objects[?((@.label=="car"|@.width>=@.height&@.type=="track"))].clientID');
}); });
test('convert filters to a json query', () => { test('convert filters to a json query', () => {
const annotationsFilter = new AnnotationsFilter(); const annotationsFilter = new AnnotationsFilter();
const [groups, query] = annotationsFilter const [groups, query] = annotationsFilter.toJSONQuery([
.toJSONQuery(['label=="person" & attr["Attribute 1"] ==attr["Attribute 2"]']); 'label=="person" & attr["Attribute 1"] ==attr["Attribute 2"]',
expect(groups).toEqual([
['label=="person"', '&', 'attr["Attribute 1"] ==attr["Attribute 2"]'],
]); ]);
expect(groups).toEqual([['label=="person"', '&', 'attr["Attribute 1"] ==attr["Attribute 2"]']]);
expect(query).toBe('$.objects[?((@.label=="person"&@.attr["Attribute 1"]==@.attr["Attribute 2"]))].clientID'); expect(query).toBe('$.objects[?((@.label=="person"&@.attr["Attribute 1"]==@.attr["Attribute 2"]))].clientID');
}); });
test('convert filters to a json query', () => { test('convert filters to a json query', () => {
const annotationsFilter = new AnnotationsFilter(); const annotationsFilter = new AnnotationsFilter();
const [groups, query] = annotationsFilter const [groups, query] = annotationsFilter.toJSONQuery([
.toJSONQuery(['label=="car" & attr["parked"]==true', 'label=="pedestrian" & width > 150']); 'label=="car" & attr["parked"]==true',
'label=="pedestrian" & width > 150',
]);
expect(groups).toEqual([ expect(groups).toEqual([
['label=="car"', '&', 'attr["parked"]==true'], ['label=="car"', '&', 'attr["parked"]==true'],
'|', '|',
['label=="pedestrian"', '&', 'width > 150'], ['label=="pedestrian"', '&', 'width > 150'],
]); ]);
expect(query).toBe('$.objects[?((@.label=="car"&@.attr["parked"]==true)|(@.label=="pedestrian"&@.width>150))].clientID'); expect(query).toBe(
'$.objects[?((@.label=="car"&@.attr["parked"]==true)|(@.label=="pedestrian"&@.width>150))].clientID',
);
}); });
test('convert filters to a json query', () => { test('convert filters to a json query', () => {
const annotationsFilter = new AnnotationsFilter(); const annotationsFilter = new AnnotationsFilter();
const [groups, query] = annotationsFilter const [groups, query] = annotationsFilter.toJSONQuery([
.toJSONQuery(['(( label==["car \\"mazda\\""]) & (attr["sunglass ( help ) es"]==true | (width > 150 | height > 150 & (clientID == serverID))))) ']); // eslint-disable-next-line
expect(groups).toEqual([[[ '(( label==["car \\"mazda\\""]) & (attr["sunglass ( help ) es"]==true | (width > 150 | height > 150 & (clientID == serverID))))) ',
]);
expect(groups).toEqual([
[
[
['label==["car `mazda`"]'], ['label==["car `mazda`"]'],
'&', '&',
['attr["sunglass ( help ) es"]==true', '|',
['width > 150', '|', 'height > 150', '&',
[ [
'clientID == serverID', 'attr["sunglass ( help ) es"]==true',
'|',
['width > 150', '|', 'height > 150', '&', ['clientID == serverID']],
], ],
], ],
], ],
]]]); ]);
expect(query).toBe('$.objects[?((((@.label==["car `mazda`"])&(@.attr["sunglass ( help ) es"]==true|(@.width>150|@.height>150&(@.clientID==serverID))))))].clientID'); expect(query).toBe(
// eslint-disable-next-line
'$.objects[?((((@.label==["car `mazda`"])&(@.attr["sunglass ( help ) es"]==true|(@.width>150|@.height>150&(@.clientID==serverID))))))].clientID',
);
}); });
}); });

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

Loading…
Cancel
Save