Merge pull request #4422 from openvinotoolkit/release-2.0.0

Release v2.0.0
main
Nikita Manovich 4 years ago committed by GitHub
commit e33db5c2c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,4 +1,4 @@
// Copyright (C) 2018-2021 Intel Corporation
// Copyright (C) 2018-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -25,8 +25,8 @@ module.exports = {
],
rules: {
'header/header': [2, 'line', [{
pattern: ' {1}Copyright \\(C\\) (?:20\\d{2}-)?2021 Intel Corporation',
template: ' Copyright (C) 2021 Intel Corporation'
pattern: ' {1}Copyright \\(C\\) (?:20\\d{2}-)?2022 Intel Corporation',
template: ' Copyright (C) 2022 Intel Corporation'
}, '', ' SPDX-License-Identifier: MIT']],
'no-plusplus': 0,
'no-continue': 0,

@ -1,5 +1,5 @@
<!---
Copyright (C) 2020 Intel Corporation
Copyright (C) 2020-2021 Intel Corporation
SPDX-License-Identifier: MIT
-->
@ -24,7 +24,7 @@ current behavior -->
to implement the addition or change -->
### Steps to Reproduce (for bugs)
<!--- Provide a link to a live example, or an unambiguous set of steps to
<!--- Provide a link to a live example or an unambiguous set of steps to
reproduce this bug. Include code to reproduce, if relevant -->
1.
1.

@ -1,11 +1,11 @@
<!---
Copyright (C) 2020-2021 Intel Corporation
Copyright (C) 2020-2022 Intel Corporation
SPDX-License-Identifier: MIT
-->
<!-- Raised an issue to propose your change (https://github.com/opencv/cvat/issues).
It will help avoiding duplication of efforts from multiple independent contributors.
It helps to avoid duplication of efforts from multiple independent contributors.
Discuss your ideas with maintainers to be sure that changes will be approved and merged.
Read the [CONTRIBUTION](https://github.com/opencv/cvat/blob/develop/CONTRIBUTING.md)
guide. -->
@ -25,10 +25,10 @@ see how your change affects other areas of the code, etc. -->
### Checklist
<!-- Go over all the following points, and put an `x` in all the boxes that apply.
If an item isn't applicable by a reason then ~~explicitly strikethrough~~ the whole
line. If you don't do that github will show incorrect process for the pull request.
line. If you don't do that github will show an incorrect process for the pull request.
If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [ ] I submit my changes into the `develop` branch
- [ ] I have added description of my changes into [CHANGELOG](https://github.com/opencv/cvat/blob/develop/CHANGELOG.md) file
- [ ] I have added a description of my changes into [CHANGELOG](https://github.com/opencv/cvat/blob/develop/CHANGELOG.md) file
- [ ] I have updated the [documentation](
https://github.com/opencv/cvat/blob/develop/README.md#documentation) accordingly
- [ ] I have added tests to cover my changes
@ -45,7 +45,7 @@ If you're unsure about any of these, don't hesitate to ask. We're here to help!
- [ ] I have updated the license header for each file (see an example below)
```python
# Copyright (C) 2021 Intel Corporation
# Copyright (C) 2022 Intel Corporation
#
# SPDX-License-Identifier: MIT
```

@ -5,19 +5,24 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- id: files
uses: jitterbit/get-changed-files@v1
continue-on-error: true
- name: Run checks
env:
PR_FILES_AM: ${{ steps.files.outputs.added_modified }}
PR_FILES_RENAMED: ${{ steps.files.outputs.renamed }}
run: |
URL="https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files"
PR_FILES=$(curl -s -X GET -G $URL | jq -r '.[] | select(.status != "removed") | .filename')
for files in $PR_FILES; do
extension="${files##*.}"
if [[ $extension == 'py' ]]; then
changed_files_bandit+=" ${files}"
fi
PR_FILES="$PR_FILES_AM $PR_FILES_RENAMED"
for FILE in $PR_FILES; do
EXTENSION="${FILE##*.}"
if [[ $EXTENSION == 'py' ]]; then
CHANGED_FILES+=" $FILE"
fi
done
if [[ ! -z ${changed_files_bandit} ]]; then
if [[ ! -z $CHANGED_FILES ]]; then
sudo apt-get --no-install-recommends install -y build-essential curl python3-dev python3-pip python3-venv
python3 -m venv .env
. .env/bin/activate
@ -25,9 +30,9 @@ jobs:
pip install bandit
mkdir -p bandit_report
echo "Bandit version: "`bandit --version | head -1`
echo "The files will be checked: "`echo ${changed_files_bandit}`
bandit ${changed_files_bandit} --exclude '**/tests/**' -a file --ini ./.bandit -f html -o ./bandit_report/bandit_checks.html
echo "Bandit version: "$(bandit --version | head -1)
echo "The files will be checked: "$(echo $CHANGED_FILES)
bandit $CHANGED_FILES --exclude '**/tests/**' -a file --ini ./.bandit -f html -o ./bandit_report/bandit_checks.html
deactivate
else
echo "No files with the \"py\" extension found"

@ -8,26 +8,32 @@ jobs:
- uses: actions/setup-node@v2
with:
node-version: '16.x'
- id: files
uses: jitterbit/get-changed-files@v1
continue-on-error: true
- name: Run checks
env:
PR_FILES_AM: ${{ steps.files.outputs.added_modified }}
PR_FILES_RENAMED: ${{ steps.files.outputs.renamed }}
run: |
URL="https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files"
PR_FILES=$(curl -s -X GET -G $URL | jq -r '.[] | select(.status != "removed") | .filename')
for files in $PR_FILES; do
extension="${files##*.}"
if [[ $extension == 'js' || $extension == 'ts' || $extension == 'jsx' || $extension == 'tsx' ]]; then
changed_files_eslint+=" ${files}"
fi
PR_FILES="$PR_FILES_AM $PR_FILES_RENAMED"
for FILE in $PR_FILES; do
EXTENSION="${FILE##*.}"
if [[ $EXTENSION == 'js' || $EXTENSION == 'ts' || $EXTENSION == 'jsx' || $EXTENSION == 'tsx' ]]; then
CHANGED_FILES+=" $FILE"
fi
done
if [[ ! -z ${changed_files_eslint} ]]; then
if [[ ! -z $CHANGED_FILES ]]; then
npm ci
cd tests && npm ci && cd ..
npm install eslint-detailed-reporter --save-dev --legacy-peer-deps
mkdir -p eslint_report
echo "ESLint version: "`npx eslint --version`
echo "The files will be checked: "`echo ${changed_files_eslint}`
npx eslint ${changed_files_eslint} -f node_modules/eslint-detailed-reporter/lib/detailed.js -o ./eslint_report/eslint_checks.html
echo "ESLint version: "$(npx eslint --version)
echo "The files will be checked: "$(echo $CHANGED_FILES)
npx eslint $CHANGED_FILES -f node_modules/eslint-detailed-reporter/lib/detailed.js -o ./eslint_report/eslint_checks.html
else
echo "No files with the \"js|ts|jsx|tsx\" extension found"
fi

@ -5,31 +5,35 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- id: files
uses: jitterbit/get-changed-files@v1
continue-on-error: true
- name: Run checks
env:
HADOLINT: "${{ github.workspace }}/hadolint"
HADOLINT_VER: "2.1.0"
VERIFICATION_LEVEL: "error"
PR_FILES_AM: ${{ steps.files.outputs.added_modified }}
PR_FILES_RENAMED: ${{ steps.files.outputs.renamed }}
run: |
URL="https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files"
PR_FILES=$(curl -s -X GET -G $URL | jq -r '.[] | select(.status != "removed") | .filename')
for file in $PR_FILES; do
if [[ ${file} =~ 'Dockerfile' ]]; then
changed_dockerfiles+=" ${file}"
fi
PR_FILES="$PR_FILES_AM $PR_FILES_RENAMED"
for FILE in $PR_FILES; do
if [[ $FILE =~ 'Dockerfile' ]]; then
CHANGED_FILES+=" $FILE"
fi
done
if [[ ! -z ${changed_dockerfiles} ]]; then
curl -sL -o ${HADOLINT} "https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VER}/hadolint-Linux-x86_64" && chmod 700 ${HADOLINT}
echo "HadoLint version: "`${HADOLINT} --version`
echo "The files will be checked: "`echo ${changed_dockerfiles}`
if [[ ! -z $CHANGED_FILES ]]; then
curl -sL -o $HADOLINT "https://github.com/hadolint/hadolint/releases/download/v$HADOLINT_VER/hadolint-Linux-x86_64" && chmod 700 $HADOLINT
echo "HadoLint version: "$($HADOLINT --version)
echo "The files will be checked: "$(echo $CHANGED_FILES)
mkdir -p hadolint_report
${HADOLINT} --no-fail --format json ${changed_dockerfiles} > ./hadolint_report/hadolint_report.json
get_verification_level=`cat ./hadolint_report/hadolint_report.json | jq -r '.[] | .level'`
for line in ${get_verification_level}; do
if [[ ${line} =~ ${VERIFICATION_LEVEL} ]]; then
$HADOLINT --no-fail --format json $CHANGED_FILES > ./hadolint_report/hadolint_report.json
GET_VERIFICATION_LEVEL=$(cat ./hadolint_report/hadolint_report.json | jq -r '.[] | .level')
for LINE in $GET_VERIFICATION_LEVEL; do
if [[ $LINE =~ $VERIFICATION_LEVEL ]]; then
pip install json2html
python ./tests/json_to_html.py ./hadolint_report/hadolint_report.json
exit 1

@ -16,6 +16,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.8'
- name: Getting SHA from the default branch
id: get-sha
run: |
@ -63,6 +66,20 @@ jobs:
cache-from: type=local,src=/tmp/cvat_cache_server
tags: openvino/cvat_server:latest
load: true
- name: Running OPA tests
run: |
curl -L -o opa https://openpolicyagent.org/downloads/v0.34.2/opa_linux_amd64_static
chmod +x ./opa
./opa test cvat/apps/iam/rules
- name: Running REST API tests
env:
API_ABOUT_PAGE: "localhost:8080/api/server/about"
run: |
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f components/serverless/docker-compose.serverless.yml -f components/analytics/docker-compose.analytics.yml up -d
/bin/bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' ${API_ABOUT_PAGE})" != "401" ]]; do sleep 5; done'
pip3 install --user -r tests/rest_api/requirements.txt
pytest tests/rest_api/
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f components/serverless/docker-compose.serverless.yml -f components/analytics/docker-compose.analytics.yml down -v
- name: Running unit tests
env:
HOST_COVERAGE_DATA_DIR: ${{ github.workspace }}
@ -88,7 +105,7 @@ jobs:
strategy:
fail-fast: false
matrix:
specs: ['actions_tasks', 'actions_tasks2', 'actions_tasks3', 'actions_objects', 'actions_objects2', 'actions_users', 'actions_projects_models', 'canvas3d_functionality', 'canvas3d_functionality_2', 'issues_prs', 'issues_prs2']
specs: ['actions_tasks', 'actions_tasks2', 'actions_tasks3', 'actions_objects', 'actions_objects2', 'actions_users', 'actions_projects_models', 'actions_organizations', 'canvas3d_functionality', 'canvas3d_functionality_2', 'issues_prs', 'issues_prs2']
steps:
- uses: actions/checkout@v2
- name: Getting SHA from the default branch
@ -165,25 +182,25 @@ jobs:
DJANGO_SU_NAME: 'admin'
DJANGO_SU_EMAIL: 'admin@localhost.company'
DJANGO_SU_PASSWORD: '12qwaszx'
API_ABOUT_PAGE: "localhost:8080/api/v1/server/about"
API_ABOUT_PAGE: "localhost:8080/api/server/about"
run: |
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f components/serverless/docker-compose.serverless.yml -f tests/docker-compose.file_share.yml up -d
/bin/bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' ${API_ABOUT_PAGE})" != "401" ]]; do sleep 5; done'
/bin/bash -c 'while [[ $(curl -s -o /dev/null -w "%{http_code}" ${API_ABOUT_PAGE}) != "401" ]]; do sleep 5; done'
docker exec -i cvat /bin/bash -c "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 ci
if [[ ${{ github.ref }} == 'refs/heads/develop' ]]; then
if [ ${{ matrix.specs }} == 'canvas3d_functionality' ] || [ ${{ matrix.specs }} == 'canvas3d_functionality_2' ]; then
npx cypress run --headed --browser chrome --config-file cypress_canvas3d.json --spec 'cypress/integration/${{ matrix.specs }}/**/*.js'
npx cypress run --headed --browser chrome --config-file cypress_canvas3d.json --spec 'cypress/integration/${{ matrix.specs }}/**/*.js,cypress/integration/remove_users_tasks_projects_organizations.js'
else
npx cypress run --browser chrome --spec 'cypress/integration/${{ matrix.specs }}/**/*.js'
npx cypress run --browser chrome --spec 'cypress/integration/${{ matrix.specs }}/**/*.js,cypress/integration/remove_users_tasks_projects_organizations.js'
fi
mv ./.nyc_output/out.json ./.nyc_output/out_${{ matrix.specs }}.json
else
if [ ${{ matrix.specs }} == 'canvas3d_functionality' ] || [ ${{ matrix.specs }} == 'canvas3d_functionality_2' ]; then
npx cypress run --headed --browser chrome --env coverage=false --config-file cypress_canvas3d.json --spec 'cypress/integration/${{ matrix.specs }}/**/*.js'
npx cypress run --headed --browser chrome --env coverage=false --config-file cypress_canvas3d.json --spec 'cypress/integration/${{ matrix.specs }}/**/*.js,cypress/integration/remove_users_tasks_projects_organizations.js'
else
npx cypress run --browser chrome --env coverage=false --spec 'cypress/integration/${{ matrix.specs }}/**/*.js'
npx cypress run --browser chrome --env coverage=false --spec 'cypress/integration/${{ matrix.specs }}/**/*.js,cypress/integration/remove_users_tasks_projects_organizations.js'
fi
fi
- name: Creating a log file from "cvat" container logs

@ -28,11 +28,11 @@ jobs:
DJANGO_SU_NAME: 'admin'
DJANGO_SU_EMAIL: 'admin@localhost.company'
DJANGO_SU_PASSWORD: '12qwaszx'
API_ABOUT_PAGE: "localhost:8080/api/v1/server/about"
API_ABOUT_PAGE: "localhost:8080/api/server/about"
run: |
docker-compose -f docker-compose.yml -f docker-compose.dev.yml build
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f components/serverless/docker-compose.serverless.yml -f tests/docker-compose.file_share.yml up -d
/bin/bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' ${API_ABOUT_PAGE})" != "401" ]]; do sleep 5; done'
/bin/bash -c 'while [[ $(curl -s -o /dev/null -w "%{http_code}" ${API_ABOUT_PAGE}) != "401" ]]; do sleep 5; done'
docker exec -i cvat /bin/bash -c "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 ci

@ -5,19 +5,24 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- id: files
uses: jitterbit/get-changed-files@v1
continue-on-error: true
- name: Run checks
env:
PR_FILES_AM: ${{ steps.files.outputs.added_modified }}
PR_FILES_RENAMED: ${{ steps.files.outputs.renamed }}
run: |
URL="https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files"
PR_FILES=$(curl -s -X GET -G $URL | jq -r '.[] | select(.status != "removed") | .filename')
for files in $PR_FILES; do
extension="${files##*.}"
if [[ $extension == 'py' ]]; then
changed_files_pylint+=" ${files}"
fi
PR_FILES="$PR_FILES_AM $PR_FILES_RENAMED"
for FILE in $PR_FILES; do
EXTENSION="${FILE##*.}"
if [[ $EXTENSION == 'py' ]]; then
CHANGED_FILES+=" $FILE"
fi
done
if [[ ! -z ${changed_files_pylint} ]]; then
if [[ ! -z $CHANGED_FILES ]]; then
sudo apt-get --no-install-recommends install -y build-essential curl python3-dev python3-pip python3-venv
python3 -m venv .env
. .env/bin/activate
@ -27,12 +32,12 @@ jobs:
pip install $(egrep "Django.*" ./cvat/requirements/base.txt)
mkdir -p pylint_report
echo "Pylint version: "`pylint --version | head -1`
echo "The files will be checked: "`echo ${changed_files_pylint}`
pylint ${changed_files_pylint} --output-format=json > ./pylint_report/pylint_checks.json || exit_code=`echo $?` || true
echo "Pylint version: "$(pylint --version | head -1)
echo "The files will be checked: "$(echo $CHANGED_FILES)
pylint $CHANGED_FILES --output-format=json > ./pylint_report/pylint_checks.json || EXIT_CODE=$(echo $?) || true
pylint-json2html -o ./pylint_report/pylint_checks.html ./pylint_report/pylint_checks.json
deactivate
exit ${exit_code}
exit $EXIT_CODE
else
echo "No files with the \"py\" extension found"
fi

@ -16,10 +16,10 @@ jobs:
DJANGO_SU_NAME: "admin"
DJANGO_SU_EMAIL: "admin@localhost.company"
DJANGO_SU_PASSWORD: "12qwaszx"
API_ABOUT_PAGE: "localhost:8080/api/v1/server/about"
API_ABOUT_PAGE: "localhost:8080/api/server/about"
run: |
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f ./tests/docker-compose.email.yml -f tests/docker-compose.file_share.yml -f components/serverless/docker-compose.serverless.yml up -d --build
/bin/bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' ${API_ABOUT_PAGE})" != "401" ]]; do sleep 5; done'
/bin/bash -c 'while [[ $(curl -s -o /dev/null -w "%{http_code}" ${API_ABOUT_PAGE}) != "401" ]]; do sleep 5; done'
docker exec -i cvat /bin/bash -c "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"
- name: End-to-end testing
run: |

@ -8,28 +8,33 @@ jobs:
- uses: actions/setup-node@v2
with:
node-version: '16.x'
- id: files
uses: jitterbit/get-changed-files@v1
continue-on-error: true
- name: Run checks
env:
PR_FILES_AM: ${{ steps.files.outputs.added_modified }}
PR_FILES_RENAMED: ${{ steps.files.outputs.renamed }}
run: |
URL="https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files"
PR_FILES=$(curl -s -X GET -G $URL | jq -r '.[] | select(.status != "removed") | .filename')
for files in $PR_FILES; do
extension="${files##*.}"
if [[ $extension == 'css' || $extension == 'scss' ]]; then
changed_files_stylelint+=" ${files}"
fi
PR_FILES="$PR_FILES_AM $PR_FILES_RENAMED"
for FILE in $PR_FILES; do
EXTENSION="${FILE##*.}"
if [[ $EXTENSION == 'css' || $EXTENSION == 'scss' ]]; then
CHANGED_FILES+=" $FILE"
fi
done
if [[ ! -z ${changed_files_stylelint} ]]; then
if [[ ! -z $CHANGED_FILES ]]; then
npm ci
mkdir -p stylelint_report
echo "StyleLint version: "`npx stylelint --version`
echo "The files will be checked: "`echo ${changed_files_stylelint}`
npx stylelint --formatter json --output-file ./stylelint_report/stylelint_report.json ${changed_files_stylelint} || exit_code=`echo $?` || true
echo "StyleLint version: "$(npx stylelint --version)
echo "The files will be checked: "$(echo $CHANGED_FILES)
npx stylelint --formatter json --output-file ./stylelint_report/stylelint_report.json $CHANGED_FILES || EXIT_CODE=$(echo $?) || true
pip install json2html
python ./tests/json_to_html.py ./stylelint_report/stylelint_report.json
exit ${exit_code}
exit $EXIT_CODE
else
echo "No files with the \"css|scss\" extension found"
fi

1
.gitignore vendored

@ -19,6 +19,7 @@ __pycache__
*.pyc
._*
.coverage
.husky/
# Ignore npm logs file
npm-debug.log*

@ -45,7 +45,7 @@
"python": "${command:python.interpreterPath}",
"program": "${workspaceRoot}/manage.py",
"env": {
"CVAT_SERVERLESS": "1"
"CVAT_SERVERLESS": "1"
},
"args": [
"runserver",
@ -62,10 +62,10 @@
"type": "pwa-chrome",
"request": "launch",
"url": "http://localhost:7000/",
"disableNetworkCache":true,
"disableNetworkCache": true,
"trace": true,
"showAsyncStacks": true,
"pathMapping":{
"pathMapping": {
"/static/engine/": "${workspaceFolder}/cvat/apps/engine/static/engine/",
"/static/dashboard/": "${workspaceFolder}/cvat/apps/dashboard/static/dashboard/",
}
@ -111,7 +111,7 @@
"request": "launch",
"justMyCode": false,
"stopOnEntry": false,
"python":"${command:python.interpreterPath}",
"python": "${command:python.interpreterPath}",
"program": "${workspaceRoot}/manage.py",
"args": [
"rqworker",
@ -193,10 +193,10 @@
"type": "node",
"request": "launch",
"name": "jest debug",
"program": "${workspaceFolder}/cvat-core/node_modules/.bin/jest",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": [
"--config",
"${workspaceFolder}/cvat-core/jest.config.js"
"--config",
"${workspaceFolder}/cvat-core/jest.config.js"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
@ -215,4 +215,4 @@
]
}
]
}
}

@ -32,5 +32,10 @@
"name": "cvat",
"database": "${workspaceFolder:cvat}/db.sqlite3"
}
]
],
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}

@ -5,6 +5,86 @@ 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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## \[2.0.0] - 2022-03-04
### Added
- Handle attributes coming from nuclio detectors (<https://github.com/openvinotoolkit/cvat/pull/3917>)
- Add additional environment variables for Nuclio configuration (<https://github.com/openvinotoolkit/cvat/pull/3894>)
- Add KITTI segmentation and detection format (<https://github.com/openvinotoolkit/cvat/pull/3757>)
- Add LFW format (<https://github.com/openvinotoolkit/cvat/pull/3770>)
- Add Cityscapes format (<https://github.com/openvinotoolkit/cvat/pull/3758>)
- Add Open Images V6 format (<https://github.com/openvinotoolkit/cvat/pull/3679>)
- Rotated bounding boxes (<https://github.com/openvinotoolkit/cvat/pull/3832>)
- Player option: Smooth image when zoom-in, enabled by default (<https://github.com/openvinotoolkit/cvat/pull/3933>)
- Google Cloud Storage support in UI (<https://github.com/openvinotoolkit/cvat/pull/3919>)
- Add project tasks pagination (<https://github.com/openvinotoolkit/cvat/pull/3910>)
- Add remove issue button (<https://github.com/openvinotoolkit/cvat/pull/3952>)
- Data sorting option (<https://github.com/openvinotoolkit/cvat/pull/3937>)
- Options to change font size & position of text labels on the canvas (<https://github.com/openvinotoolkit/cvat/pull/3972>)
- Add "tag" return type for automatic annotation in Nuclio (<https://github.com/openvinotoolkit/cvat/pull/3896>)
- Helm chart: Make user-data-permission-fix optional (<https://github.com/openvinotoolkit/cvat/pull/3994>)
- Advanced identity access management system, using open policy agent (<https://github.com/openvinotoolkit/cvat/pull/3788>)
- Organizations to create "shared space" for different groups of users (<https://github.com/openvinotoolkit/cvat/pull/3788>)
- Dataset importing to a project (<https://github.com/openvinotoolkit/cvat/pull/3790>)
- User is able to customize information that text labels show (<https://github.com/openvinotoolkit/cvat/pull/4029>)
- Support for uploading manifest with any name (<https://github.com/openvinotoolkit/cvat/pull/4041>)
- Added information about OpenVINO toolkit to login page (<https://github.com/openvinotoolkit/cvat/pull/4077>)
- Support for working with ellipses (<https://github.com/openvinotoolkit/cvat/pull/4062>)
- Add several flags to task creation CLI (<https://github.com/openvinotoolkit/cvat/pull/4119>)
- Add YOLOv5 serverless function for automatic annotation (<https://github.com/openvinotoolkit/cvat/pull/4178>)
- Add possibility to change git repository and git export format from already created task (<https://github.com/openvinotoolkit/cvat/pull/3886>)
- Basic page with jobs list, basic filtration to this list (<https://github.com/openvinotoolkit/cvat/pull/4258>)
- Added OpenCV.js TrackerMIL as tracking tool (<https://github.com/openvinotoolkit/cvat/pull/4200>)
- Ability to continue working from the latest frame where an annotator was before (<https://github.com/openvinotoolkit/cvat/pull/4297>)
- `GET /api/jobs/<id>/commits` was implemented (<https://github.com/openvinotoolkit/cvat/pull/4368>)
- Advanced filtration and sorting for a list of jobs (<https://github.com/openvinotoolkit/cvat/pull/4319>)
### Changed
- Users don't have access to a task object anymore if they are assigned only on some jobs of the task (<https://github.com/openvinotoolkit/cvat/pull/3788>)
- Different resources (tasks, projects) are not visible anymore for all CVAT instance users by default (<https://github.com/openvinotoolkit/cvat/pull/3788>)
- API versioning scheme: using accept header versioning instead of namespace versioning (<https://github.com/openvinotoolkit/cvat/pull/4239>)
- Replaced 'django_sendfile' with 'django_sendfile2' (<https://github.com/openvinotoolkit/cvat/pull/4267>)
- Use drf-spectacular instead of drf-yasg for swagger documentation (<https://github.com/openvinotoolkit/cvat/pull/4210>)
### Deprecated
- Job field "status" is not used in UI anymore, but it has not been removed from the database yet (<https://github.com/openvinotoolkit/cvat/pull/3788>)
### Removed
- Review rating, reviewer field from the job instance (use assignee field together with stage field instead) (<https://github.com/openvinotoolkit/cvat/pull/3788>)
- Training django app (<https://github.com/openvinotoolkit/cvat/pull/4330>)
- v1 api version support (<https://github.com/openvinotoolkit/cvat/pull/4332>)
### Fixed
- Fixed Interaction handler keyboard handlers (<https://github.com/openvinotoolkit/cvat/pull/3881>)
- Points of invisible shapes are visible in autobordering (<https://github.com/openvinotoolkit/cvat/pull/3931>)
- Order of the label attributes in the object item details(<https://github.com/openvinotoolkit/cvat/pull/3945>)
- Order of labels in tasks and projects (<https://github.com/openvinotoolkit/cvat/pull/3987>)
- Fixed task creating with large files via webpage (<https://github.com/openvinotoolkit/cvat/pull/3692>)
- Added information to export CVAT_HOST when performing local installation for accessing over network (<https://github.com/openvinotoolkit/cvat/pull/4014>)
- Fixed possible color collisions in the generated colormap (<https://github.com/openvinotoolkit/cvat/pull/4007>)
- Original pdf file is deleted when using share (<https://github.com/openvinotoolkit/cvat/pull/3967>)
- Order in an annotation file(<https://github.com/openvinotoolkit/cvat/pull/4087>)
- Fixed task data upload progressbar (<https://github.com/openvinotoolkit/cvat/pull/4134>)
- Email in org invitations is case sensitive (<https://github.com/openvinotoolkit/cvat/pull/4153>)
- Caching for tasks and jobs can lead to an exception if its assignee user is removed (<https://github.com/openvinotoolkit/cvat/pull/4165>)
- Added intelligent function when paste labels to another task (<https://github.com/openvinotoolkit/cvat/pull/4161>)
- Uncaught TypeError: this.el.node.getScreenCTM() is null in Firefox (<https://github.com/openvinotoolkit/cvat/pull/4175>)
- Bug: canvas is busy when start playing, start resizing a shape and do not release the mouse cursor (<https://github.com/openvinotoolkit/cvat/pull/4151>)
- Bug: could not receive frame N. TypeError: Cannot read properties of undefined (reding "filename") (<https://github.com/openvinotoolkit/cvat/pull/4187>)
- Cannot choose a dataset format for a linked repository if a task type is annotation (<https://github.com/openvinotoolkit/cvat/pull/4203>)
- Fixed tus upload error over https (<https://github.com/openvinotoolkit/cvat/pull/4154>)
- Issues disappear when rescale a browser (<https://github.com/openvinotoolkit/cvat/pull/4189>)
- Auth token key is not returned when registering without email verification (<https://github.com/openvinotoolkit/cvat/pull/4092>)
- Error in create project from backup for standard 3D annotation (<https://github.com/openvinotoolkit/cvat/pull/4160>)
- Annotations search does not work correctly in some corner cases (when use complex properties with width, height) (<https://github.com/openvinotoolkit/cvat/pull/4198>)
- Kibana requests are not proxied due to django-revproxy incompatibility with Django >3.2.x (<https://github.com/openvinotoolkit/cvat/issues/4085>)
- Content type for getting frame with tasks/{id}/data/ endpoint (<https://github.com/openvinotoolkit/cvat/pull/4333>)
- Bug: Permission error occured when accessing the comments of a specific issue (<https://github.com/openvinotoolkit/cvat/issues/4416>)
### Security
- Updated ELK to 6.8.23 which uses log4j 2.17.1 (<https://github.com/openvinotoolkit/cvat/pull/4206>)
- Added validation for URLs which used as remote data source (<https://github.com/openvinotoolkit/cvat/pull/4387>)
## \[1.7.0] - 2021-11-15
### Added
@ -13,13 +93,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- interactor: add HRNet interactive segmentation serverless function (<https://github.com/openvinotoolkit/cvat/pull/3740>)
- Added GPU implementation for SiamMask, reworked tracking approach (<https://github.com/openvinotoolkit/cvat/pull/3571>)
- Progress bar for manifest creating (<https://github.com/openvinotoolkit/cvat/pull/3712>)
- IAM: Open Policy Agent integration (<https://github.com/openvinotoolkit/cvat/pull/3788>)
- Add a tutorial on attaching cloud storage AWS-S3 (<https://github.com/openvinotoolkit/cvat/pull/3745>)
and Azure Blob Container (<https://github.com/openvinotoolkit/cvat/pull/3778>)
- The feature to remove annotations in a specified range of frames (<https://github.com/openvinotoolkit/cvat/pull/3617>)
- Project backup/restore (<https://github.com/openvinotoolkit/cvat/pull/3852>)
### Changed
- UI tracking has been reworked (<https://github.com/openvinotoolkit/cvat/pull/3571>)
- Updated Django till 3.2.7 (automatic AppConfig discovery)
- Manifest generation: Reduce creating time (<https://github.com/openvinotoolkit/cvat/pull/3712>)
- Migration from NPM 6 to NPM 7 (<https://github.com/openvinotoolkit/cvat/pull/3773>)
- Update Datumaro dependency to 0.2.0 (<https://github.com/openvinotoolkit/cvat/pull/3813>)

@ -44,7 +44,7 @@ RUN curl -sL https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 --outp
# Install requirements
RUN python3 -m venv /opt/venv
ENV PATH="/opt/venv/bin:${PATH}"
RUN python3 -m pip install --no-cache-dir -U pip==21.0.1 setuptools==53.0.0 wheel==0.36.2
RUN python3 -m pip install --no-cache-dir -U pip==22.0.2 setuptools==60.6.0 wheel==0.37.1
COPY cvat/requirements/ /tmp/requirements/
RUN DATUMARO_HEADLESS=1 python3 -m pip install --no-cache-dir -r /tmp/requirements/${DJANGO_CONFIGURATION}.txt
@ -152,7 +152,6 @@ USER ${USER}
WORKDIR ${HOME}
RUN mkdir data share media keys logs /tmp/supervisord
RUN python3 manage.py collectstatic
EXPOSE 8080
ENTRYPOINT ["/usr/bin/supervisord"]

@ -37,6 +37,5 @@ RUN npm run build:cvat-ui
FROM nginx:mainline-alpine
# Replace default.conf configuration to remove unnecessary rules
RUN sed -i "s/}/application\/wasm wasm;\n}/g" /etc/nginx/mime.types
COPY cvat-ui/react_nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=cvat-ui /tmp/cvat-ui/dist /usr/share/nginx/html/

@ -1,6 +1,6 @@
MIT License
Copyright (C) 2018-2021 Intel Corporation
Copyright (C) 2018-2022 Intel Corporation
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"),
@ -28,4 +28,4 @@ See https://www.ffmpeg.org/legal.html. You are solely responsible
for determining if your use of FFmpeg requires any
additional licenses. Intel is not responsible for obtaining any
such licenses, nor liable for any licensing fees due in
connection with your use of FFmpeg.
connection with your use of FFmpeg.

@ -70,6 +70,10 @@ For more information about supported formats look at the
| [VGGFace2](https://github.com/ox-vgg/vgg_face2) | X | X |
| [Market-1501](https://www.aitribune.com/dataset/2018051063) | X | X |
| [ICDAR13/15](https://rrc.cvc.uab.es/?ch=2) | X | X |
| [Open Images V6](https://storage.googleapis.com/openimages/web/index.html) | X | X |
| [Cityscapes](https://www.cityscapes-dataset.com/login/) | X | X |
| [KITTI](http://www.cvlibs.net/datasets/kitti/) | X | X |
| [LFW](http://vis-www.cs.umass.edu/lfw/) | X | X |
<!--lint enable maximum-line-length-->
@ -86,6 +90,7 @@ For more information about supported formats look at the
| [Object reidentification](/serverless/openvino/omz/intel/person-reidentification-retail-300/nuclio) | reid | OpenVINO | X | |
| [Semantic segmentation for ADAS](/serverless/openvino/omz/intel/semantic-segmentation-adas-0001/nuclio) | detector | OpenVINO | X | |
| [Text detection v4](/serverless/openvino/omz/intel/text-detection-0004/nuclio) | detector | OpenVINO | X | |
| [YOLO v5](/serverless/pytorch/ultralytics/yolov5/nuclio) | detector | PyTorch | X | |
| [SiamMask](/serverless/pytorch/foolwood/siammask/nuclio) | tracker | PyTorch | X | X |
| [f-BRS](/serverless/pytorch/saic-vul/fbrs/nuclio) | interactor | PyTorch | X | |
| [HRNet](/serverless/pytorch/saic-vul/hrnet/nuclio) | interactor | PyTorch | | X |
@ -93,6 +98,7 @@ For more information about supported formats look at the
| [Faster RCNN](/serverless/tensorflow/faster_rcnn_inception_v2_coco/nuclio) | detector | TensorFlow | X | X |
| [Mask RCNN](/serverless/tensorflow/matterport/mask_rcnn/nuclio) | detector | TensorFlow | X | X |
| [RetinaNet](serverless/pytorch/facebookresearch/detectron2/retinanet/nuclio) | detector | PyTorch | X | X |
| [Face Detection](/serverless/openvino/omz/intel/face-detection-0205/nuclio) | detector | OpenVINO | X | |
<!--lint enable maximum-line-length-->
@ -132,6 +138,21 @@ additional licenses. Intel is not responsible for obtaining any
such licenses, nor liable for any licensing fees due in
connection with your use of FFmpeg.
## Partners
- [Onepanel](https://github.com/onepanelio/core) is an open source
vision AI platform that fully integrates CVAT with scalable data processing
and parallelized training pipelines.
- [DataIsKey](https://dataiskey.eu/annotation-tool/) uses CVAT as their prime data labeling tool
to offer annotation services for projects of any size.
- [Human Protocol](https://hmt.ai) uses CVAT as a way of adding annotation service to the human protocol.
- [Cogito Tech LLC](https://bit.ly/3klT0h6), a Human-in-the-Loop Workforce Solutions Provider, used CVAT
in annotation of about 5,000 images for a brand operating in the fashion segment.
- [FiftyOne](https://fiftyone.ai) is an open-source dataset curation and model analysis
tool for visualizing, exploring, and improving computer vision datasets and models that is
[tightly integrated](https://voxel51.com/docs/fiftyone/integrations/cvat.html) with CVAT
for annotation and label refinement.
## Questions
CVAT usage related questions or unclear concepts can be posted in our
@ -156,14 +177,6 @@ Other ways to ask questions and get our support:
- [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/)
## Projects using CVAT
- [Onepanel](https://github.com/onepanelio/core) is an open source
vision AI platform that fully integrates CVAT with scalable data processing
and parallelized training pipelines.
- [DataIsKey](https://dataiskey.eu/annotation-tool/) uses CVAT as their prime data labeling tool
to offer annotation services for projects of any size.
- [Human Protocol](https://hmt.ai) uses CVAT as a way of adding annotation service to the human protocol.
<!-- prettier-ignore-start -->
<!-- Badges -->

@ -8,7 +8,7 @@ services:
build:
context: ./components/analytics/elasticsearch
args:
ELK_VERSION: 6.4.0
ELK_VERSION: 6.8.23
volumes:
- cvat_events:/usr/share/elasticsearch/data
restart: always
@ -21,7 +21,7 @@ services:
build:
context: ./components/analytics/kibana
args:
ELK_VERSION: 6.4.0
ELK_VERSION: 6.8.23
depends_on: ['elasticsearch']
restart: always
@ -62,7 +62,7 @@ services:
build:
context: ./components/analytics/logstash
args:
ELK_VERSION: 6.4.0
ELK_VERSION: 6.8.23
http_proxy: ${http_proxy}
https_proxy: ${https_proxy}
environment:
@ -76,10 +76,15 @@ services:
environment:
DJANGO_LOG_SERVER_HOST: logstash
DJANGO_LOG_SERVER_PORT: 8080
CVAT_ANALYTICS: 1
traefik:
environment:
CVAT_HOST: ${CVAT_HOST:-localhost}
DJANGO_LOG_VIEWER_HOST: kibana
DJANGO_LOG_VIEWER_PORT: 5601
CVAT_ANALYTICS: 1
no_proxy: kibana,logstash,nuclio,${no_proxy}
volumes:
- ./components/analytics/kibana_conf.yml:/etc/traefik/rules/kibana_conf.yml:ro
volumes:
cvat_events:

@ -1,9 +1,13 @@
# Copyright (C) 2021 Intel Corporation
#
# SPDX-License-Identifier: MIT
#/usr/bin/env python
import os
import argparse
import requests
import json
from time import sleep
import requests
def import_resources(host, port, cfg_file):
with open(cfg_file, 'r') as f:
@ -27,6 +31,19 @@ def import_saved_object(host, port, _type, _id, data):
headers={'kbn-xsrf': 'true'})
request.raise_for_status()
def wait_for_status(host, port, status='green', max_attempts=10, delay=3):
for _ in range(max_attempts):
response = requests.get('http://{}:{}/api/status'.format(host, port))
if response.status_code != 200:
sleep(delay)
continue
response = response.json()
if status == response['status']['overall']['state']:
return True
return False
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='import Kibana 6.x resources',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
@ -37,4 +54,8 @@ if __name__ == '__main__':
parser.add_argument('-H', '--host', metavar='HOST', default='kibana',
help='host of Kibana instance')
args = parser.parse_args()
import_resources(args.host, args.port, args.export_file)
if wait_for_status(args.host, args.port):
import_resources(args.host, args.port, args.export_file)
else:
exit('Cannot setup Kibana objects')

@ -0,0 +1,30 @@
http:
routers:
kibana:
entryPoints:
- web
middlewares:
- analytics-auth
- strip-prefix
service: kibana
rule: Host(`{{ env "CVAT_HOST" }}`) && PathPrefix(`/analytics`)
middlewares:
analytics-auth:
forwardauth:
address: http://cvat:8080/analytics
authRequestHeaders:
- "Cookie"
- "Authorization"
strip-prefix:
stripprefix:
prefixes:
- /analytics
services:
kibana:
loadBalancer:
servers:
- url: http://{{ env "DJANGO_LOG_VIEWER_HOST" }}:{{ env "DJANGO_LOG_VIEWER_PORT" }}
passHostHeader: false

@ -21,7 +21,6 @@ services:
cvat:
environment:
CVAT_SERVERLESS: 1
no_proxy: kibana,logstash,nuclio,${no_proxy}
volumes:
cvat_events:

@ -137,6 +137,7 @@ Canvas itself handles:
cancel(): void;
configure(configuration: Configuration): void;
isAbleToChangeFrame(): boolean;
destroy(): void;
readonly geometry: Geometry;
}
@ -183,12 +184,14 @@ Standard JS events are used.
- canvas.zoomstart
- canvas.zoomstop
- canvas.zoom
- canvas.reshape
- canvas.fit
- canvas.dragshape => {id: number}
- canvas.roiselected => {points: number[]}
- canvas.resizeshape => {id: number}
- canvas.contextmenu => { mouseEvent: MouseEvent, objectState: ObjectState, pointID: number }
- canvas.error => { exception: Error }
- canvas.destroy
```
### WEB
@ -239,6 +242,7 @@ canvas.draw({
| bitmap() | + | + | + | + | + | + | + | + | + | + | + |
| setZLayer() | + | + | + | + | + | + | + | + | + | + | + |
| setupReviewROIs() | + | + | + | + | + | + | + | + | + | + | + |
| destroy() | + | + | + | + | + | + | + | + | + | + | + |
<!--lint enable maximum-line-length-->

@ -1,21 +1,35 @@
{
"name": "cvat-canvas",
"version": "2.8.0",
"version": "2.13.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "cvat-canvas",
"version": "2.8.0",
"version": "2.13.1",
"license": "MIT",
"dependencies": {
"@types/polylabel": "^1.0.5",
"polylabel": "^1.1.0",
"svg.draggable.js": "2.2.2",
"svg.draw.js": "^2.0.4",
"svg.js": "2.7.1",
"svg.resize.js": "1.4.3",
"svg.select.js": "3.0.1"
},
"devDependencies": {}
}
},
"node_modules/@types/polylabel": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/polylabel/-/polylabel-1.0.5.tgz",
"integrity": "sha512-gnaNmo1OJiYNBFAZMZdqLZ3hKx2ee4ksAzqhKWBxuQ61PmhINHMcvIqsGmyCD1WFKCkwRt9NFhMSmKE6AgYY+w=="
},
"node_modules/polylabel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/polylabel/-/polylabel-1.1.0.tgz",
"integrity": "sha512-bxaGcA40sL3d6M4hH72Z4NdLqxpXRsCFk8AITYg6x1rn1Ei3izf00UMLklerBZTO49aPA3CYrIwVulx2Bce2pA==",
"dependencies": {
"tinyqueue": "^2.0.3"
}
},
"node_modules/svg.draggable.js": {
"version": "2.2.2",
@ -77,9 +91,27 @@
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/tinyqueue": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz",
"integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA=="
}
},
"dependencies": {
"@types/polylabel": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/polylabel/-/polylabel-1.0.5.tgz",
"integrity": "sha512-gnaNmo1OJiYNBFAZMZdqLZ3hKx2ee4ksAzqhKWBxuQ61PmhINHMcvIqsGmyCD1WFKCkwRt9NFhMSmKE6AgYY+w=="
},
"polylabel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/polylabel/-/polylabel-1.1.0.tgz",
"integrity": "sha512-bxaGcA40sL3d6M4hH72Z4NdLqxpXRsCFk8AITYg6x1rn1Ei3izf00UMLklerBZTO49aPA3CYrIwVulx2Bce2pA==",
"requires": {
"tinyqueue": "^2.0.3"
}
},
"svg.draggable.js": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",
@ -127,6 +159,11 @@
"requires": {
"svg.js": "^2.6.5"
}
},
"tinyqueue": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz",
"integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA=="
}
}
}

@ -1,6 +1,6 @@
{
"name": "cvat-canvas",
"version": "2.8.0",
"version": "2.13.1",
"description": "Part of Computer Vision Annotation Tool which presents its canvas library",
"main": "src/canvas.ts",
"scripts": {
@ -15,8 +15,9 @@
"not IE 11",
"> 2%"
],
"devDependencies": {},
"dependencies": {
"@types/polylabel": "^1.0.5",
"polylabel": "^1.1.0",
"svg.draggable.js": "2.2.2",
"svg.draw.js": "^2.0.4",
"svg.js": "2.7.1",

@ -37,7 +37,6 @@ polyline.cvat_shape_drawing_opacity {
.cvat_canvas_text {
font-weight: bold;
font-size: 1.2em;
fill: white;
cursor: default;
font-family: Calibri, Candara, Segoe, 'Segoe UI', Optima, Arial, sans-serif;
@ -47,7 +46,6 @@ polyline.cvat_shape_drawing_opacity {
}
.cvat_canvas_text_description {
font-size: 14px;
fill: yellow;
font-style: oblique 40deg;
}
@ -76,7 +74,6 @@ polyline.cvat_shape_drawing_opacity {
}
.cvat_canvas_issue_region {
display: none;
stroke-width: 0;
}
@ -137,6 +134,10 @@ polyline.cvat_canvas_shape_splitting {
stroke-dasharray: 5;
}
.svg_select_points_rot {
fill: white;
}
.cvat_canvas_shape .svg_select_points,
.cvat_canvas_shape .cvat_canvas_cuboid_projections {
stroke-dasharray: none;
@ -166,8 +167,9 @@ polyline.cvat_canvas_shape_splitting {
.cvat_canvas_removable_interaction_point {
cursor:
url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAxMCAxMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEgMUw5IDlNMSA5TDkgMSIgc3Ryb2tlPSJibGFjayIvPgo8L3N2Zz4K')
10 10,
url(
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAxMCAxMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEgMUw5IDlNMSA5TDkgMSIgc3Ryb2tlPSJibGFjayIvPgo8L3N2Zz4K'
) 10 10,
auto;
}
@ -224,6 +226,17 @@ polyline.cvat_canvas_shape_splitting {
}
}
.cvat_canvas_pixelized {
image-rendering: optimizeSpeed; /* Legal fallback */
image-rendering: -moz-crisp-edges; /* Firefox */
image-rendering: -o-crisp-edges; /* Opera */
image-rendering: -webkit-optimize-contrast; /* Safari */
image-rendering: optimize-contrast; /* CSS3 Proposed */
image-rendering: crisp-edges; /* CSS4 Proposed */
image-rendering: pixelated; /* CSS4 Proposed */
-ms-interpolation-mode: nearest-neighbor; /* IE8+ */
}
#cvat_canvas_wrapper {
width: calc(100% - 10px);
height: calc(100% - 10px);
@ -268,6 +281,8 @@ polyline.cvat_canvas_shape_splitting {
}
#cvat_canvas_bitmap {
@extend .cvat_canvas_pixelized;
pointer-events: none;
position: absolute;
z-index: 4;

@ -237,7 +237,8 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
const currentClientID = this.currentShape.node.dataset.originClientId;
const shapes = Array.from(this.frameContent.getElementsByClassName('cvat_canvas_shape')).filter(
(shape: HTMLElement): boolean => +shape.getAttribute('clientID') !== this.currentID,
(shape: HTMLElement): boolean => +shape.getAttribute('clientID') !== this.currentID &&
!shape.classList.contains('cvat_canvas_hidden'),
);
const transformedShapes = shapes
.map((shape: HTMLElement): TransformedShape | null => {
@ -252,6 +253,10 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
let points = '';
if (shape.tagName === 'polyline' || shape.tagName === 'polygon') {
points = shape.getAttribute('points');
} else if (shape.tagName === 'ellipse') {
const cx = +shape.getAttribute('cx');
const cy = +shape.getAttribute('cy');
points = `${cx},${cy}`;
} else if (shape.tagName === 'rect') {
const x = +shape.getAttribute('x');
const y = +shape.getAttribute('y');

@ -1,4 +1,4 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -29,7 +29,7 @@ const CanvasVersion = pjson.version;
interface Canvas {
html(): HTMLDivElement;
setup(frameData: any, objectStates: any[], zLayer?: number): void;
setupIssueRegions(issueRegions: Record<number, number[]>): void;
setupIssueRegions(issueRegions: Record<number, { hidden: boolean; points: number[] }>): void;
activate(clientID: number | null, attributeID?: number): void;
rotate(rotationAngle: number): void;
focus(clientID: number, padding?: number): void;
@ -53,6 +53,7 @@ interface Canvas {
cancel(): void;
configure(configuration: Configuration): void;
isAbleToChangeFrame(): boolean;
destroy(): void;
readonly geometry: Geometry;
}
@ -76,7 +77,7 @@ class CanvasImpl implements Canvas {
this.model.setup(frameData, objectStates, zLayer);
}
public setupIssueRegions(issueRegions: Record<number, number[]>): void {
public setupIssueRegions(issueRegions: Record<number, { hidden: boolean; points: number[] }>): void {
this.model.setupIssueRegions(issueRegions);
}
@ -163,6 +164,10 @@ class CanvasImpl implements Canvas {
public get geometry(): Geometry {
return this.model.geometry;
}
public destroy(): void {
this.model.destroy();
}
}
export type InteractionData = _InteractionData;

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -19,7 +19,7 @@ import {
export interface CanvasController {
readonly objects: any[];
readonly issueRegions: Record<number, number[]>;
readonly issueRegions: Record<number, { hidden: boolean; points: number[] }>;
readonly zLayer: number | null;
readonly focusData: FocusData;
readonly activeElement: ActiveElement;
@ -123,7 +123,7 @@ export class CanvasControllerImpl implements CanvasController {
return this.model.zLayer;
}
public get issueRegions(): Record<number, number[]> {
public get issueRegions(): Record<number, { hidden: boolean; points: number[] }> {
return this.model.issueRegions;
}

@ -1,7 +1,8 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import consts from './consts';
import { MasterImpl } from './master';
export interface Size {
@ -52,8 +53,12 @@ export enum CuboidDrawingMethod {
}
export interface Configuration {
smoothImage?: boolean;
autoborders?: boolean;
displayAllText?: boolean;
textFontSize?: number;
textPosition?: 'auto' | 'center';
textContent?: string;
undefinedAttrValue?: string;
showProjections?: boolean;
forceDisableEditing?: boolean;
@ -146,6 +151,7 @@ export enum UpdateReasons {
ZOOM_CANVAS = 'zoom_canvas',
CONFIG_UPDATED = 'config_updated',
DATA_FAILED = 'data_failed',
DESTROY = 'destroy',
}
export enum Mode {
@ -166,7 +172,7 @@ export enum Mode {
export interface CanvasModel {
readonly imageBitmap: boolean;
readonly image: Image | null;
readonly issueRegions: Record<number, number[]>;
readonly issueRegions: Record<number, { hidden: boolean; points: number[] }>;
readonly objects: any[];
readonly zLayer: number | null;
readonly gridSize: Size;
@ -187,7 +193,7 @@ export interface CanvasModel {
move(topOffset: number, leftOffset: number): void;
setup(frameData: any, objectStates: any[], zLayer: number): void;
setupIssueRegions(issueRegions: Record<number, number[]>): void;
setupIssueRegions(issueRegions: Record<number, { hidden: boolean; points: number[] }>): void;
activate(clientID: number | null, attributeID: number | null): void;
rotate(rotationAngle: number): void;
focus(clientID: number, padding: number): void;
@ -210,6 +216,7 @@ export interface CanvasModel {
isAbleToChangeFrame(): boolean;
configure(configuration: Configuration): void;
cancel(): void;
destroy(): void;
}
export class CanvasModelImpl extends MasterImpl implements CanvasModel {
@ -227,7 +234,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
gridSize: Size;
left: number;
objects: any[];
issueRegions: Record<number, number[]>;
issueRegions: Record<number, { hidden: boolean; points: number[] }>;
scale: number;
top: number;
zLayer: number | null;
@ -258,6 +265,9 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
displayAllText: false,
autoborders: false,
undefinedAttrValue: '',
textContent: 'id,label,attributes,source,descriptions',
textPosition: 'auto',
textFontSize: consts.DEFAULT_SHAPE_TEXT_SIZE,
},
imageBitmap: false,
image: null,
@ -343,6 +353,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.notify(UpdateReasons.FITTED_CANVAS);
this.notify(UpdateReasons.OBJECTS_UPDATED);
this.notify(UpdateReasons.ISSUE_REGIONS_UPDATED);
}
public bitmap(enabled: boolean): void {
@ -435,7 +446,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
});
}
public setupIssueRegions(issueRegions: Record<number, number[]>): void {
public setupIssueRegions(issueRegions: Record<number, { hidden: boolean; points: number[] }>): void {
this.data.issueRegions = issueRegions;
this.notify(UpdateReasons.ISSUE_REGIONS_UPDATED);
}
@ -644,29 +655,42 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.data.configuration.displayAllText = configuration.displayAllText;
}
if (typeof configuration.textFontSize === 'number' && configuration.textFontSize >= consts.MINIMUM_TEXT_FONT_SIZE) {
this.data.configuration.textFontSize = configuration.textFontSize;
}
if (['auto', 'center'].includes(configuration.textPosition)) {
this.data.configuration.textPosition = configuration.textPosition;
}
if (typeof configuration.textContent === 'string') {
const splitted = configuration.textContent.split(',').filter((entry: string) => !!entry);
if (splitted.every((entry: string) => ['id', 'label', 'attributes', 'source', 'descriptions'].includes(entry))) {
this.data.configuration.textContent = configuration.textContent;
}
}
if (typeof configuration.showProjections === 'boolean') {
this.data.configuration.showProjections = configuration.showProjections;
}
if (typeof configuration.autoborders === 'boolean') {
this.data.configuration.autoborders = configuration.autoborders;
}
if (typeof configuration.smoothImage === 'boolean') {
this.data.configuration.smoothImage = configuration.smoothImage;
}
if (typeof configuration.undefinedAttrValue === 'string') {
this.data.configuration.undefinedAttrValue = configuration.undefinedAttrValue;
}
if (typeof configuration.forceDisableEditing === 'boolean') {
this.data.configuration.forceDisableEditing = configuration.forceDisableEditing;
}
if (typeof configuration.intelligentPolygonCrop === 'boolean') {
this.data.configuration.intelligentPolygonCrop = configuration.intelligentPolygonCrop;
}
if (typeof configuration.forceFrameUpdate === 'boolean') {
this.data.configuration.forceFrameUpdate = configuration.forceFrameUpdate;
}
if (typeof configuration.creationOpacity === 'number') {
this.data.configuration.creationOpacity = configuration.creationOpacity;
}
@ -685,6 +709,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.notify(UpdateReasons.CANCEL);
}
public destroy(): void {
this.notify(UpdateReasons.DESTROY);
}
public get configuration(): Configuration {
return { ...this.data.configuration };
}
@ -729,7 +757,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
return this.data.image;
}
public get issueRegions(): Record<number, number[]> {
public get issueRegions(): Record<number, { hidden: boolean; points: number[] }> {
return { ...this.data.issueRegions };
}

@ -1,7 +1,8 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import polylabel from 'polylabel';
import * as SVG from 'svg.js';
import 'svg.draggable.js';
@ -24,6 +25,7 @@ import {
translateToSVG,
translateFromSVG,
translateToCanvas,
translateFromCanvas,
pointsToNumberArray,
parsePoints,
displayShapeSize,
@ -31,6 +33,8 @@ import {
vectorLength,
ShapeSizeElement,
DrawnState,
rotate2DPoints,
readPointsFromShape,
} from './shared';
import {
CanvasModel,
@ -85,7 +89,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
private interactionHandler: InteractionHandler;
private activeElement: ActiveElement;
private configuration: Configuration;
private serviceFlags: {
private snapToAngleResize: number;
private innerObjectsFlags: {
drawHidden: Record<number, boolean>;
};
@ -109,7 +114,40 @@ export class CanvasViewImpl implements CanvasView, Listener {
private translateFromCanvas(points: number[]): number[] {
const { offset } = this.controller.geometry;
return points.map((coord: number): number => coord - offset);
return translateFromCanvas(offset, points);
}
private translatePointsFromRotatedShape(shape: SVG.Shape, points: number[]): number[] {
const { rotation } = shape.transform();
// currently shape is rotated and shifted somehow additionally (css transform property)
// let's remove rotation to get correct transformation matrix (element -> screen)
// correct means that we do not consider points to be rotated
// because rotation property is stored separately and already saved
shape.rotate(0);
const result = [];
try {
// get each point and apply a couple of matrix transformation to it
const point = this.content.createSVGPoint();
// matrix to convert from ELEMENT file system to CLIENT coordinate system
const ctm = ((shape.node as any) as SVGRectElement | SVGPolygonElement | SVGPolylineElement).getScreenCTM();
// matrix to convert from CLIENT coordinate system to CANVAS coordinate system
const ctm1 = this.content.getScreenCTM().inverse();
// NOTE: I tried to use element.getCTM(), but this way does not work on firefox
for (let i = 0; i < points.length; i += 2) {
point.x = points[i];
point.y = points[i + 1];
let transformedPoint = point.matrixTransform(ctm);
transformedPoint = transformedPoint.matrixTransform(ctm1);
result.push(transformedPoint.x, transformedPoint.y);
}
} finally {
shape.rotate(rotation);
}
return result;
}
private stringifyToCanvas(points: number[]): string {
@ -122,12 +160,12 @@ export class CanvasViewImpl implements CanvasView, Listener {
}, '');
}
private isServiceHidden(clientID: number): boolean {
return this.serviceFlags.drawHidden[clientID] || false;
private isInnerHidden(clientID: number): boolean {
return this.innerObjectsFlags.drawHidden[clientID] || false;
}
private setupServiceHidden(clientID: number, value: boolean): void {
this.serviceFlags.drawHidden[clientID] = value;
private setupInnerFlags(clientID: number, path: 'drawHidden', value: boolean): void {
this.innerObjectsFlags[path][clientID] = value;
const shape = this.svgShapes[clientID];
const text = this.svgTexts[clientID];
const state = this.drawnStates[clientID];
@ -143,7 +181,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
text.addClass('cvat_canvas_hidden');
}
} else {
delete this.serviceFlags.drawHidden[clientID];
delete this.innerObjectsFlags[path][clientID];
if (state) {
if (!state.outside && !state.hidden) {
@ -199,11 +237,12 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
}
private onDrawDone(data: object | null, duration: number, continueDraw?: boolean): void {
const hiddenBecauseOfDraw = Object.keys(this.serviceFlags.drawHidden).map((_clientID): number => +_clientID);
private onDrawDone(data: any | null, duration: number, continueDraw?: boolean): void {
const hiddenBecauseOfDraw = Object.keys(this.innerObjectsFlags.drawHidden)
.map((_clientID): number => +_clientID);
if (hiddenBecauseOfDraw.length) {
for (const hidden of hiddenBecauseOfDraw) {
this.setupServiceHidden(hidden, false);
this.setupInnerFlags(hidden, 'drawHidden', false);
}
}
@ -256,7 +295,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
}
private onEditDone(state: any, points: number[]): void {
private onEditDone(state: any, points: number[], rotation?: number): void {
if (state && points) {
const event: CustomEvent = new CustomEvent('canvas.edited', {
bubbles: false,
@ -264,6 +303,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
detail: {
state,
points,
rotation: typeof rotation === 'number' ? rotation : state.rotation,
},
});
@ -388,7 +428,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
private onFindObject(e: MouseEvent): void {
if (e.which === 1 || e.which === 0) {
if (e.button === 0) {
const { offset } = this.controller.geometry;
const [x, y] = translateToSVG(this.content, [e.clientX, e.clientY]);
const event: CustomEvent = new CustomEvent('canvas.find', {
@ -483,7 +523,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.gridPath.setAttribute('stroke-width', `${consts.BASE_GRID_WIDTH / this.geometry.scale}px`);
// Transform all shape points
for (const element of window.document.getElementsByClassName('svg_select_points')) {
for (const element of [
...window.document.getElementsByClassName('svg_select_points'),
...window.document.getElementsByClassName('svg_select_points_rot'),
]) {
element.setAttribute('stroke-width', `${consts.POINTS_STROKE_WIDTH / this.geometry.scale}`);
element.setAttribute('r', `${consts.BASE_POINT_SIZE / this.geometry.scale}`);
}
@ -563,7 +606,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
}
private setupIssueRegions(issueRegions: Record<number, number[]>): void {
private setupIssueRegions(issueRegions: Record<number, { hidden: boolean; points: number[] }>): void {
for (const issueRegion of Object.keys(this.drawnIssueRegions)) {
if (!(issueRegion in issueRegions) || !+issueRegion) {
this.drawnIssueRegions[+issueRegion].remove();
@ -573,7 +616,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
for (const issueRegion of Object.keys(issueRegions)) {
if (issueRegion in this.drawnIssueRegions) continue;
const points = this.translateToCanvas(issueRegions[+issueRegion]);
const points = this.translateToCanvas(issueRegions[+issueRegion].points);
if (points.length === 2) {
this.drawnIssueRegions[+issueRegion] = this.adoptedContent
.circle((consts.BASE_POINT_SIZE * 3 * 2) / this.geometry.scale)
@ -613,6 +656,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
'stroke-width': `${consts.BASE_STROKE_WIDTH / this.geometry.scale}`,
});
}
if (issueRegions[+issueRegion].hidden) {
this.drawnIssueRegions[+issueRegion].style({ display: 'none' });
}
}
}
@ -641,16 +688,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.deactivate();
}
for (const state of deleted) {
if (state.clientID in this.svgTexts) {
this.svgTexts[state.clientID].remove();
}
this.svgShapes[state.clientID].off('click.canvas');
this.svgShapes[state.clientID].remove();
delete this.drawnStates[state.clientID];
}
this.deleteObjects(deleted);
this.addObjects(created);
this.updateObjects(updated);
this.sortObjects();
@ -744,12 +782,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (e.button !== 0) return;
e.preventDefault();
const pointID = Array.prototype.indexOf.call(
((e.target as HTMLElement).parentElement as HTMLElement).children,
e.target,
);
if (this.activeElement.clientID !== null) {
const pointID = Array.prototype.indexOf.call(
((e.target as HTMLElement).parentElement as HTMLElement).children,
e.target,
);
const [state] = this.controller.objects.filter(
(_state: any): boolean => _state.clientID === this.activeElement.clientID,
);
@ -821,13 +858,12 @@ export class CanvasViewImpl implements CanvasView, Listener {
e.preventDefault();
};
const getGeometry = (): Geometry => this.geometry;
if (value) {
const getGeometry = (): Geometry => this.geometry;
(shape as any).selectize(value, {
deepSelect: true,
pointSize: (2 * consts.BASE_POINT_SIZE) / this.geometry.scale,
rotationPoint: false,
rotationPoint: shape.type === 'rect' || shape.type === 'ellipse',
pointType(cx: number, cy: number): SVG.Circle {
const circle: SVG.Circle = this.nested
.circle(this.options.pointSize)
@ -874,8 +910,45 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (handler && handler.nested) {
handler.nested.fill(shape.attr('fill'));
}
const [rotationPoint] = window.document.getElementsByClassName('svg_select_points_rot');
if (rotationPoint && !rotationPoint.children.length) {
const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
title.textContent = 'Hold Shift to snap angle';
rotationPoint.appendChild(title);
}
}
private onShiftKeyDown = (e: KeyboardEvent): void => {
if (!e.repeat && e.code.toLowerCase().includes('shift')) {
this.snapToAngleResize = consts.SNAP_TO_ANGLE_RESIZE_SHIFT;
if (this.activeElement) {
const shape = this.svgShapes[this.activeElement.clientID];
if (shape && shape.hasClass('cvat_canvas_shape_activated')) {
(shape as any).resize({ snapToAngle: this.snapToAngleResize });
}
}
}
};
private onShiftKeyUp = (e: KeyboardEvent): void => {
if (e.code.toLowerCase().includes('shift') && this.activeElement) {
this.snapToAngleResize = consts.SNAP_TO_ANGLE_RESIZE_DEFAULT;
if (this.activeElement) {
const shape = this.svgShapes[this.activeElement.clientID];
if (shape && shape.hasClass('cvat_canvas_shape_activated')) {
(shape as any).resize({ snapToAngle: this.snapToAngleResize });
}
}
}
};
private onMouseUp = (event: MouseEvent): void => {
if (event.button === 0 || event.button === 1) {
this.controller.disableDrag();
}
};
public constructor(model: CanvasModel & Master, controller: CanvasController) {
this.controller = controller;
this.geometry = controller.geometry;
@ -889,7 +962,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
};
this.configuration = model.configuration;
this.mode = Mode.IDLE;
this.serviceFlags = {
this.snapToAngleResize = consts.SNAP_TO_ANGLE_RESIZE_DEFAULT;
this.innerObjectsFlags = {
drawHidden: {},
};
@ -1046,11 +1120,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
});
window.document.addEventListener('mouseup', (event): void => {
if (event.which === 1 || event.which === 2) {
this.controller.disableDrag();
}
});
window.document.addEventListener('mouseup', this.onMouseUp);
window.document.addEventListener('keydown', this.onShiftKeyDown);
window.document.addEventListener('keyup', this.onShiftKeyUp);
this.content.addEventListener('wheel', (event): void => {
if (event.ctrlKey) return;
@ -1096,15 +1168,15 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (reason === UpdateReasons.CONFIG_UPDATED) {
const { activeElement } = this;
this.deactivate();
const { configuration } = model;
if (model.configuration.displayAllText && !this.configuration.displayAllText) {
if (configuration.displayAllText && !this.configuration.displayAllText) {
for (const i in this.drawnStates) {
if (!(i in this.svgTexts)) {
this.svgTexts[i] = this.addText(this.drawnStates[i]);
this.updateTextPosition(this.svgTexts[i], this.svgShapes[i]);
}
}
} else if (model.configuration.displayAllText === false && this.configuration.displayAllText) {
} else if (configuration.displayAllText === false && this.configuration.displayAllText) {
for (const i in this.drawnStates) {
if (i in this.svgTexts && Number.parseInt(i, 10) !== activeElement.clientID) {
this.svgTexts[i].remove();
@ -1113,7 +1185,40 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
}
this.configuration = model.configuration;
const recreateText = configuration.textContent !== this.configuration.textContent;
const updateTextPosition = configuration.displayAllText !== this.configuration.displayAllText ||
configuration.textFontSize !== this.configuration.textFontSize ||
configuration.textPosition !== this.configuration.textPosition ||
recreateText;
if (configuration.smoothImage === true) {
this.background.classList.remove('cvat_canvas_pixelized');
} else if (configuration.smoothImage === false) {
this.background.classList.add('cvat_canvas_pixelized');
}
this.configuration = configuration;
if (recreateText) {
const states = this.controller.objects;
for (const key of Object.keys(this.drawnStates)) {
const clientID = +key;
const [state] = states.filter((_state: any) => _state.clientID === clientID);
if (clientID in this.svgTexts) {
this.svgTexts[clientID].remove();
delete this.svgTexts[clientID];
if (state) this.svgTexts[clientID] = this.addText(state);
}
}
}
if (updateTextPosition) {
for (const i in this.drawnStates) {
if (i in this.svgTexts) {
this.updateTextPosition(this.svgTexts[i], this.svgShapes[i]);
}
}
}
this.activate(activeElement);
this.editHandler.configurate(this.configuration);
this.drawHandler.configurate(this.configuration);
@ -1162,8 +1267,15 @@ export class CanvasViewImpl implements CanvasView, Listener {
} else if (reason === UpdateReasons.FITTED_CANVAS) {
// Canvas geometry is going to be changed. Old object positions aren't valid any more
this.setupObjects([]);
this.setupIssueRegions({});
this.moveCanvas();
this.resizeCanvas();
this.canvas.dispatchEvent(
new CustomEvent('canvas.reshape', {
bubbles: false,
cancelable: true,
}),
);
} else if ([UpdateReasons.IMAGE_ZOOMED, UpdateReasons.IMAGE_FITTED].includes(reason)) {
this.moveCanvas();
this.transformCanvas();
@ -1258,7 +1370,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.canvas.style.cursor = 'crosshair';
this.mode = Mode.DRAW;
if (typeof data.redraw === 'number') {
this.setupServiceHidden(data.redraw, true);
this.setupInnerFlags(data.redraw, 'drawHidden', true);
}
this.drawHandler.draw(data, this.geometry);
} else {
@ -1358,6 +1470,18 @@ export class CanvasViewImpl implements CanvasView, Listener {
},
});
this.canvas.dispatchEvent(event);
} else if (reason === UpdateReasons.DESTROY) {
this.canvas.dispatchEvent(
new CustomEvent('canvas.destroy', {
bubbles: false,
cancelable: true,
}),
);
window.document.removeEventListener('keydown', this.onShiftKeyDown);
window.document.removeEventListener('keyup', this.onShiftKeyUp);
window.document.removeEventListener('mouseup', this.onMouseUp);
this.interactionHandler.destroy();
}
if (model.imageBitmap && [UpdateReasons.IMAGE_CHANGED, UpdateReasons.OBJECTS_UPDATED].includes(reason)) {
@ -1377,6 +1501,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
const states = this.controller.objects;
const ctx = this.bitmap.getContext('2d');
ctx.imageSmoothingEnabled = false;
if (ctx) {
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, width, height);
@ -1384,31 +1509,34 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (state.hidden || state.outside) continue;
ctx.fillStyle = 'white';
if (['rectangle', 'polygon', 'cuboid'].includes(state.shapeType)) {
let points = [];
let points = [...state.points];
if (state.shapeType === 'rectangle') {
points = [
state.points[0], // xtl
state.points[1], // ytl
state.points[2], // xbr
state.points[1], // ytl
state.points[2], // xbr
state.points[3], // ybr
state.points[0], // xtl
state.points[3], // ybr
];
points = rotate2DPoints(
points[0] + (points[2] - points[0]) / 2,
points[1] + (points[3] - points[1]) / 2,
state.rotation,
[
points[0], // xtl
points[1], // ytl
points[2], // xbr
points[1], // ytl
points[2], // xbr
points[3], // ybr
points[0], // xtl
points[3], // ybr
],
);
} else if (state.shapeType === 'cuboid') {
points = [
state.points[0],
state.points[1],
state.points[4],
state.points[5],
state.points[8],
state.points[9],
state.points[12],
state.points[13],
points[0],
points[1],
points[4],
points[5],
points[8],
points[9],
points[12],
points[13],
];
} else {
points = [...state.points];
}
ctx.beginPath();
ctx.moveTo(points[0], points[1]);
@ -1419,6 +1547,14 @@ export class CanvasViewImpl implements CanvasView, Listener {
ctx.fill();
}
if (state.shapeType === 'ellipse') {
const [cx, cy, rightX, topY] = state.points;
ctx.beginPath();
ctx.ellipse(cx, cy, rightX - cx, cy - topY, (state.rotation * Math.PI) / 180.0, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
}
if (state.shapeType === 'cuboid') {
for (let i = 0; i < 5; i++) {
const points = [
@ -1454,6 +1590,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
lock: state.lock,
shapeType: state.shapeType,
points: [...state.points],
rotation: state.rotation,
attributes: { ...state.attributes },
descriptions: [...state.descriptions],
zOrder: state.zOrder,
@ -1470,7 +1607,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
const drawnState = this.drawnStates[clientID];
const shape = this.svgShapes[state.clientID];
const text = this.svgTexts[state.clientID];
const isInvisible = state.hidden || state.outside || this.isServiceHidden(state.clientID);
const isInvisible = state.hidden || state.outside || this.isInnerHidden(state.clientID);
if (drawnState.hidden !== state.hidden || drawnState.outside !== state.outside) {
if (isInvisible) {
@ -1513,6 +1650,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.activate(activeElement);
}
if (drawnState.rotation) {
// need to rotate it back before changing points
shape.untransform();
}
if (
state.points.length !== drawnState.points.length ||
state.points.some((p: number, id: number): boolean => p !== drawnState.points[id])
@ -1528,6 +1670,12 @@ export class CanvasViewImpl implements CanvasView, Listener {
width: xbr - xtl,
height: ybr - ytl,
});
} else if (state.shapeType === 'ellipse') {
const [cx, cy] = translatedPoints;
const [rx, ry] = [translatedPoints[2] - cx, cy - translatedPoints[3]];
shape.attr({
cx, cy, rx, ry,
});
} else {
const stringified = this.stringifyToCanvas(translatedPoints);
if (state.shapeType !== 'cuboid') {
@ -1542,6 +1690,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
}
if (state.rotation) {
// now, when points changed, need to rotate it to new angle
shape.rotate(state.rotation);
}
const stateDescriptions = state.descriptions;
const drawnStateDescriptions = drawnState.descriptions;
@ -1574,6 +1727,22 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
}
private deleteObjects(states: any[]): void {
for (const state of states) {
if (state.clientID in this.svgTexts) {
this.svgTexts[state.clientID].remove();
delete this.svgTexts[state.clientID];
}
this.svgShapes[state.clientID].fire('remove');
this.svgShapes[state.clientID].off('click');
this.svgShapes[state.clientID].off('remove');
this.svgShapes[state.clientID].remove();
delete this.drawnStates[state.clientID];
delete this.svgShapes[state.clientID];
}
}
private addObjects(states: any[]): void {
const { displayAllText } = this.configuration;
for (const state of states) {
@ -1592,6 +1761,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.svgShapes[state.clientID] = this.addPolyline(stringified, state);
} else if (state.shapeType === 'points') {
this.svgShapes[state.clientID] = this.addPoints(stringified, state);
} else if (state.shapeType === 'ellipse') {
this.svgShapes[state.clientID] = this.addEllipse(stringified, state);
} else if (state.shapeType === 'cuboid') {
this.svgShapes[state.clientID] = this.addCuboid(stringified, state);
} else {
@ -1785,24 +1956,35 @@ export class CanvasViewImpl implements CanvasView, Listener {
.on('dragstart', (): void => {
this.mode = Mode.DRAG;
hideText();
(shape as any).on('remove.drag', (): void => {
this.mode = Mode.IDLE;
// disable internal drag events of SVG.js
window.dispatchEvent(new MouseEvent('mouseup'));
});
})
.on('dragend', (e: CustomEvent): void => {
showText();
(shape as any).off('remove.drag');
this.mode = Mode.IDLE;
showText();
const p1 = e.detail.handler.startPoints.point;
const p2 = e.detail.p;
const delta = 1;
const { offset } = this.controller.geometry;
const dx2 = (p1.x - p2.x) ** 2;
const dy2 = (p1.y - p2.y) ** 2;
if (Math.sqrt(dx2 + dy2) >= delta) {
const points = pointsToNumberArray(
shape.attr('points') || `${shape.attr('x')},${shape.attr('y')} ` +
`${shape.attr('x') + shape.attr('width')},` +
`${shape.attr('y') + shape.attr('height')}`,
).map((x: number): number => x - offset);
// these points does not take into account possible transformations, applied on the element
// so, if any (like rotation) we need to map them to canvas coordinate space
let points = readPointsFromShape(shape);
// let's keep current points, but they could be rewritten in updateObjects
this.drawnStates[clientID].points = this.translateFromCanvas(points);
this.drawnStates[state.clientID].points = points;
const { rotation } = shape.transform();
if (rotation) {
points = this.translatePointsFromRotatedShape(shape, points);
}
points = this.translateFromCanvas(points);
this.canvas.dispatchEvent(
new CustomEvent('canvas.dragshape', {
bubbles: false,
@ -1837,18 +2019,33 @@ export class CanvasViewImpl implements CanvasView, Listener {
let shapeSizeElement: ShapeSizeElement | null = null;
let resized = false;
const resizeFinally = (): void => {
if (shapeSizeElement) {
shapeSizeElement.rm();
shapeSizeElement = null;
}
this.mode = Mode.IDLE;
};
(shape as any)
.resize({
snapToGrid: 0.1,
snapToAngle: this.snapToAngleResize,
})
.on('resizestart', (): void => {
this.mode = Mode.RESIZE;
resized = false;
hideDirection();
hideText();
if (state.shapeType === 'rectangle') {
if (state.shapeType === 'rectangle' || state.shapeType === 'ellipse') {
shapeSizeElement = displayShapeSize(this.adoptedContent, this.adoptedText);
}
(shape as any).on('remove.resize', () => {
// disable internal resize events of SVG.js
window.dispatchEvent(new MouseEvent('mouseup'));
resizeFinally();
});
})
.on('resizing', (): void => {
resized = true;
@ -1857,25 +2054,29 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
})
.on('resizedone', (): void => {
if (shapeSizeElement) {
shapeSizeElement.rm();
}
(shape as any).off('remove.resize');
resizeFinally();
showDirection();
showText();
if (resized) {
let rotation = shape.transform().rotation || 0;
this.mode = Mode.IDLE;
// be sure, that rotation in range [0; 360]
while (rotation < 0) rotation += 360;
rotation %= 360;
if (resized) {
const { offset } = this.controller.geometry;
// these points does not take into account possible transformations, applied on the element
// so, if any (like rotation) we need to map them to canvas coordinate space
let points = readPointsFromShape(shape);
const points = pointsToNumberArray(
shape.attr('points') || `${shape.attr('x')},${shape.attr('y')} ` +
`${shape.attr('x') + shape.attr('width')},` +
`${shape.attr('y') + shape.attr('height')}`,
).map((x: number): number => x - offset);
// let's keep current points, but they could be rewritten in updateObjects
this.drawnStates[clientID].points = this.translateFromCanvas(points);
this.drawnStates[clientID].rotation = rotation;
if (rotation) {
points = this.translatePointsFromRotatedShape(shape, points);
}
this.drawnStates[state.clientID].points = points;
// points = this.translateFromCanvas(points);
this.canvas.dispatchEvent(
new CustomEvent('canvas.resizeshape', {
bubbles: false,
@ -1885,7 +2086,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
},
}),
);
this.onEditDone(state, points);
this.onEditDone(state, this.translateFromCanvas(points), rotation);
}
});
@ -1928,40 +2129,87 @@ export class CanvasViewImpl implements CanvasView, Listener {
// Update text position after corresponding box has been moved, resized, etc.
private updateTextPosition(text: SVG.Text, shape: SVG.Shape): void {
if (text.node.style.display === 'none') return; // wrong transformation matrix
let box = (shape.node as any).getBBox();
// Translate the whole box to the client coordinate system
const [x1, y1, x2, y2]: number[] = translateFromSVG(this.content, [
box.x,
box.y,
box.x + box.width,
box.y + box.height,
]);
const { textFontSize, textPosition } = this.configuration;
box = {
x: Math.min(x1, x2),
y: Math.min(y1, y2),
width: Math.max(x1, x2) - Math.min(x1, x2),
height: Math.max(y1, y2) - Math.min(y1, y2),
};
text.untransform();
text.style({ 'font-size': `${textFontSize}px` });
const { rotation } = shape.transform();
// Find the best place for a text
let [clientX, clientY]: number[] = [box.x + box.width, box.y];
if (
clientX + ((text.node as any) as SVGTextElement)
.getBBox().width + consts.TEXT_MARGIN > this.canvas.offsetWidth
) {
[clientX, clientY] = [box.x, box.y];
}
// Translate back to text SVG
const [x, y]: number[] = translateToSVG(this.text, [
clientX + consts.TEXT_MARGIN,
clientY + consts.TEXT_MARGIN,
let [clientX, clientY, clientCX, clientCY]: number[] = [0, 0, 0, 0];
if (textPosition === 'center') {
let cx = 0;
let cy = 0;
if (shape.type === 'rect') {
// for rectangle finding a center is simple
cx = +shape.attr('x') + +shape.attr('width') / 2;
cy = +shape.attr('y') + +shape.attr('height') / 2;
} else if (shape.type === 'ellipse') {
// even simpler for ellipses
cx = +shape.attr('cx');
cy = +shape.attr('cy');
} else {
// for polyshapes we use special algorithm
const points = parsePoints(pointsToNumberArray(shape.attr('points')));
[cx, cy] = polylabel([points.map((point) => [point.x, point.y])]);
}
[clientX, clientY] = translateFromSVG(this.content, [cx, cy]);
// center is exactly clientX, clientY
clientCX = clientX;
clientCY = clientY;
} else {
let box = (shape.node as any).getBBox();
// Translate the whole box to the client coordinate system
const [x1, y1, x2, y2]: number[] = translateFromSVG(this.content, [
box.x,
box.y,
box.x + box.width,
box.y + box.height,
]);
clientCX = x1 + (x2 - x1) / 2;
clientCY = y1 + (y2 - y1) / 2;
box = {
x: Math.min(x1, x2),
y: Math.min(y1, y2),
width: Math.max(x1, x2) - Math.min(x1, x2),
height: Math.max(y1, y2) - Math.min(y1, y2),
};
// first try to put to the top right corner
[clientX, clientY] = [box.x + box.width, box.y];
if (
clientX + ((text.node as any) as SVGTextElement)
.getBBox().width + consts.TEXT_MARGIN > this.canvas.offsetWidth
) {
// if out of visible area, try to put text to top left corner
[clientX, clientY] = [box.x, box.y];
}
}
// Translate found coordinates to text SVG
const [x, y, rotX, rotY]: number[] = translateToSVG(this.text, [
clientX + (textPosition === 'auto' ? consts.TEXT_MARGIN : 0),
clientY + (textPosition === 'auto' ? consts.TEXT_MARGIN : 0),
clientCX,
clientCY,
]);
const textBBox = ((text.node as any) as SVGTextElement).getBBox();
// Finally draw a text
text.move(x, y);
if (textPosition === 'center') {
text.move(x - textBBox.width / 2, y - textBBox.height / 2);
} else {
text.move(x, y);
}
if (rotation) {
text.rotate(rotation, rotX, rotY);
}
for (const tspan of (text.lines() as any).members) {
tspan.attr('x', text.attr('x'));
}
@ -1969,6 +2217,14 @@ export class CanvasViewImpl implements CanvasView, Listener {
private addText(state: any): SVG.Text {
const { undefinedAttrValue } = this.configuration;
const content = this.configuration.textContent;
const withID = content.includes('id');
const withAttr = content.includes('attributes');
const withLabel = content.includes('label');
const withSource = content.includes('source');
const withDescriptions = content.includes('descriptions');
const textFontSize = this.configuration.textFontSize || 12;
const {
label, clientID, attributes, source, descriptions,
} = state;
@ -1979,29 +2235,36 @@ export class CanvasViewImpl implements CanvasView, Listener {
return this.adoptedText
.text((block): void => {
block.tspan(`${label.name} ${clientID} (${source})`).style('text-transform', 'uppercase');
for (const desc of descriptions) {
block
.tspan(`${desc}`)
.attr({
dy: '1em',
x: 0,
})
.addClass('cvat_canvas_text_description');
block.tspan(`${withLabel ? label.name : ''} ${withID ? clientID : ''} ${withSource ? `(${source})` : ''}`).style({
'text-transform': 'uppercase',
});
if (withDescriptions) {
for (const desc of descriptions) {
block
.tspan(`${desc}`)
.attr({
dy: '1em',
x: 0,
})
.addClass('cvat_canvas_text_description');
}
}
for (const attrID of Object.keys(attributes)) {
const value = attributes[attrID] === undefinedAttrValue ? '' : attributes[attrID];
block
.tspan(`${attrNames[attrID]}: ${value}`)
.attr({
attrID,
dy: '1em',
x: 0,
})
.addClass('cvat_canvas_text_attribute');
if (withAttr) {
for (const attrID of Object.keys(attributes)) {
const value = attributes[attrID] === undefinedAttrValue ? '' : attributes[attrID];
block
.tspan(`${attrNames[attrID]}: ${value}`)
.attr({
attrID,
dy: '1em',
x: 0,
})
.addClass('cvat_canvas_text_attribute');
}
}
})
.move(0, 0)
.style({ 'font-size': textFontSize })
.addClass('cvat_canvas_text');
}
@ -2023,11 +2286,15 @@ export class CanvasViewImpl implements CanvasView, Listener {
.move(xtl, ytl)
.addClass('cvat_canvas_shape');
if (state.rotation) {
rect.rotate(state.rotation);
}
if (state.occluded) {
rect.addClass('cvat_canvas_shape_occluded');
}
if (state.hidden || state.outside || this.isServiceHidden(state.clientID)) {
if (state.hidden || state.outside || this.isInnerHidden(state.clientID)) {
rect.addClass('cvat_canvas_hidden');
}
@ -2053,7 +2320,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
polygon.addClass('cvat_canvas_shape_occluded');
}
if (state.hidden || state.outside || this.isServiceHidden(state.clientID)) {
if (state.hidden || state.outside || this.isInnerHidden(state.clientID)) {
polygon.addClass('cvat_canvas_hidden');
}
@ -2079,7 +2346,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
polyline.addClass('cvat_canvas_shape_occluded');
}
if (state.hidden || state.outside || this.isServiceHidden(state.clientID)) {
if (state.hidden || state.outside || this.isInnerHidden(state.clientID)) {
polyline.addClass('cvat_canvas_hidden');
}
@ -2106,7 +2373,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
cube.addClass('cvat_canvas_shape_occluded');
}
if (state.hidden || state.outside || this.isServiceHidden(state.clientID)) {
if (state.hidden || state.outside || this.isInnerHidden(state.clientID)) {
cube.addClass('cvat_canvas_hidden');
}
@ -2139,6 +2406,39 @@ export class CanvasViewImpl implements CanvasView, Listener {
return group;
}
private addEllipse(points: string, state: any): SVG.Rect {
const [cx, cy, rightX, topY] = points.split(/[/,\s]/g).map((coord) => +coord);
const [rx, ry] = [rightX - cx, cy - topY];
const rect = this.adoptedContent
.ellipse(rx * 2, ry * 2)
.attr({
clientID: state.clientID,
'color-rendering': 'optimizeQuality',
id: `cvat_canvas_shape_${state.clientID}`,
fill: state.color,
'shape-rendering': 'geometricprecision',
stroke: state.color,
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'data-z-order': state.zOrder,
})
.center(cx, cy)
.addClass('cvat_canvas_shape');
if (state.rotation) {
rect.rotate(state.rotation);
}
if (state.occluded) {
rect.addClass('cvat_canvas_shape_occluded');
}
if (state.hidden || state.outside || this.isInnerHidden(state.clientID)) {
rect.addClass('cvat_canvas_hidden');
}
return rect;
}
private addPoints(points: string, state: any): SVG.PolyLine {
const shape = this.adoptedContent
.polyline(points)
@ -2155,7 +2455,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
const group = this.setupPoints(shape, state);
if (state.hidden || state.outside || this.isServiceHidden(state.clientID)) {
if (state.hidden || state.outside || this.isInnerHidden(state.clientID)) {
group.addClass('cvat_canvas_hidden');
}

@ -17,6 +17,10 @@ const UNDEFINED_ATTRIBUTE_VALUE = '__undefined__';
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';
const BASE_PATTERN_SIZE = 5;
const SNAP_TO_ANGLE_RESIZE_DEFAULT = 0.1;
const SNAP_TO_ANGLE_RESIZE_SHIFT = 15;
const DEFAULT_SHAPE_TEXT_SIZE = 12;
const MINIMUM_TEXT_FONT_SIZE = 8;
export default {
BASE_STROKE_WIDTH,
@ -33,4 +37,8 @@ export default {
UNDEFINED_ATTRIBUTE_VALUE,
ARROW_PATH,
BASE_PATTERN_SIZE,
SNAP_TO_ANGLE_RESIZE_DEFAULT,
SNAP_TO_ANGLE_RESIZE_SHIFT,
DEFAULT_SHAPE_TEXT_SIZE,
MINIMUM_TEXT_FONT_SIZE,
};

@ -10,30 +10,28 @@ export enum Orientation {
RIGHT = 'right',
}
function line(p1: Point, p2: Point): number[] {
const a = p1.y - p2.y;
const b = p2.x - p1.x;
const c = b * p1.y + a * p1.x;
return [a, b, c];
}
export function intersection(p1: Point, p2: Point, p3: Point, p4: Point): Point | null {
const L1 = line(p1, p2);
const L2 = line(p3, p4);
const D = L1[0] * L2[1] - L1[1] * L2[0];
const Dx = L1[2] * L2[1] - L1[1] * L2[2];
const Dy = L1[0] * L2[2] - L1[2] * L2[0];
let x = null;
let y = null;
if (Math.abs(D) > Number.EPSILON) {
x = Dx / D;
y = Dy / D;
return { x, y };
// Check if none of the lines are of length 0
const { x: x1, y: y1 } = p1;
const { x: x2, y: y2 } = p2;
const { x: x3, y: y3 } = p3;
const { x: x4, y: y4 } = p4;
if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) {
return null;
}
const denominator = ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1));
// Lines are parallel
if (Math.abs(denominator) < Number.EPSILON) {
return null;
}
return null;
const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator;
// Return a object with the x and y coordinates of the intersection
return { x: x1 + ua * (x2 - x1), y: y1 + ua * (y2 - y1) };
}
export class Equation {

@ -1,4 +1,4 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -12,10 +12,11 @@ import {
displayShapeSize,
ShapeSizeElement,
stringifyPoints,
pointsToNumberArray,
BBox,
Box,
Point,
readPointsFromShape,
clamp,
} from './shared';
import Crosshair from './crosshair';
import consts from './consts';
@ -37,6 +38,38 @@ interface FinalCoordinates {
box: Box;
}
function checkConstraint(shapeType: string, points: number[], box: Box | null = null): boolean {
if (shapeType === 'rectangle') {
const [xtl, ytl, xbr, ybr] = points;
return (xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD;
}
if (shapeType === 'polygon') {
return (box.xbr - box.xtl) * (box.ybr - box.ytl) >= consts.AREA_THRESHOLD && points.length >= 3 * 2;
}
if (shapeType === 'polyline') {
return (box.xbr - box.xtl >= consts.SIZE_THRESHOLD ||
box.ybr - box.ytl >= consts.SIZE_THRESHOLD) && points.length >= 2 * 2;
}
if (shapeType === 'points') {
return points.length > 2 || (points.length === 2 && points[0] !== 0 && points[1] !== 0);
}
if (shapeType === 'ellipse') {
const [rx, ry] = [points[2] - points[0], points[1] - points[3]];
return rx * ry * Math.PI >= consts.AREA_THRESHOLD;
}
if (shapeType === 'cuboid') {
return points.length === 4 * 2 || points.length === 8 * 2 ||
(points.length === 2 * 2 && (points[2] - points[0]) * (points[3] - points[1]) >= consts.AREA_THRESHOLD);
}
return false;
}
export class DrawHandlerImpl implements DrawHandler {
// callback is used to notify about creating new shape
private onDrawDone: (data: object | null, duration?: number, continueDraw?: boolean) => void;
@ -62,24 +95,37 @@ export class DrawHandlerImpl implements DrawHandler {
private pointsGroup: SVG.G | null;
private shapeSizeElement: ShapeSizeElement;
private getFinalRectCoordinates(bbox: BBox): number[] {
private getFinalEllipseCoordinates(points: number[], fitIntoFrame: boolean): number[] {
const { offset } = this.geometry;
const [cx, cy, rightX, topY] = points.map((coord: number) => coord - offset);
const [rx, ry] = [rightX - cx, cy - topY];
const frameWidth = this.geometry.image.width;
const frameHeight = this.geometry.image.height;
const [fitCX, fitCY] = fitIntoFrame ?
[clamp(cx, 0, frameWidth), clamp(cy, 0, frameHeight)] : [cx, cy];
const [fitRX, fitRY] = fitIntoFrame ?
[Math.min(rx, frameWidth - cx, cx), Math.min(ry, frameHeight - cy, cy)] : [rx, ry];
return [fitCX, fitCY, fitCX + fitRX, fitCY - fitRY];
}
private getFinalRectCoordinates(points: number[], fitIntoFrame: boolean): number[] {
const frameWidth = this.geometry.image.width;
const frameHeight = this.geometry.image.height;
const { offset } = this.geometry;
let [xtl, ytl, xbr, ybr] = [bbox.x, bbox.y, bbox.x + bbox.width, bbox.y + bbox.height].map(
(coord: number): number => coord - offset,
);
let [xtl, ytl, xbr, ybr] = points.map((coord: number): number => coord - offset);
xtl = Math.min(Math.max(xtl, 0), frameWidth);
xbr = Math.min(Math.max(xbr, 0), frameWidth);
ytl = Math.min(Math.max(ytl, 0), frameHeight);
ybr = Math.min(Math.max(ybr, 0), frameHeight);
if (fitIntoFrame) {
xtl = Math.min(Math.max(xtl, 0), frameWidth);
xbr = Math.min(Math.max(xbr, 0), frameWidth);
ytl = Math.min(Math.max(ytl, 0), frameHeight);
ybr = Math.min(Math.max(ybr, 0), frameHeight);
}
return [xtl, ytl, xbr, ybr];
}
private getFinalPolyshapeCoordinates(targetPoints: number[]): FinalCoordinates {
private getFinalPolyshapeCoordinates(targetPoints: number[], fitIntoFrame: boolean): FinalCoordinates {
const { offset } = this.geometry;
let points = targetPoints.map((coord: number): number => coord - offset);
const box = {
@ -184,8 +230,10 @@ export class DrawHandlerImpl implements DrawHandler {
return resultPoints;
};
points = crop(points, Direction.Horizontal);
points = crop(points, Direction.Vertical);
if (fitIntoFrame) {
points = crop(points, Direction.Horizontal);
points = crop(points, Direction.Vertical);
}
for (let i = 0; i < points.length - 1; i += 2) {
box.xtl = Math.min(box.xtl, points[i]);
@ -299,7 +347,6 @@ export class DrawHandlerImpl implements DrawHandler {
this.initialized = false;
this.canvas.off('mousedown.draw');
this.canvas.off('mousemove.draw');
this.canvas.off('click.draw');
if (this.pointsGroup) {
this.pointsGroup.remove();
@ -349,21 +396,19 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawInstance = this.canvas.rect();
this.drawInstance
.on('drawstop', (e: Event): void => {
const bbox = (e.target as SVGRectElement).getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
const points = readPointsFromShape((e.target as any as { instance: SVG.Rect }).instance);
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(points, true);
const { shapeType, redraw: clientID } = this.drawData;
this.release();
if (this.canceled) return;
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
this.onDrawDone(
{
clientID,
shapeType,
points: [xtl, ytl, xbr, ybr],
},
Date.now() - this.startTimestamp,
);
if (checkConstraint('rectangle', [xtl, ytl, xbr, ybr])) {
this.onDrawDone({
clientID,
shapeType,
points: [xtl, ytl, xbr, ybr],
},
Date.now() - this.startTimestamp);
}
})
.on('drawupdate', (): void => {
@ -376,6 +421,59 @@ export class DrawHandlerImpl implements DrawHandler {
});
}
private drawEllipse(): void {
this.drawInstance = (this.canvas as any).ellipse()
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity,
});
const initialPoint: {
x: number;
y: number;
} = {
x: null,
y: null,
};
this.canvas.on('mousedown.draw', (e: MouseEvent): void => {
if (initialPoint.x === null || initialPoint.y === null) {
const translated = translateToSVG(this.canvas.node as any as SVGSVGElement, [e.clientX, e.clientY]);
[initialPoint.x, initialPoint.y] = translated;
} else {
const points = this.getFinalEllipseCoordinates(readPointsFromShape(this.drawInstance), false);
const { shapeType, redraw: clientID } = this.drawData;
this.release();
if (this.canceled) return;
if (checkConstraint('ellipse', points)) {
this.onDrawDone(
{
clientID,
shapeType,
points,
},
Date.now() - this.startTimestamp,
);
}
}
});
this.canvas.on('mousemove.draw', (e: MouseEvent): void => {
if (initialPoint.x !== null && initialPoint.y !== null) {
const translated = translateToSVG(this.canvas.node as any as SVGSVGElement, [e.clientX, e.clientY]);
const rx = Math.abs(translated[0] - initialPoint.x) / 2;
const ry = Math.abs(translated[1] - initialPoint.y) / 2;
const cx = initialPoint.x + rx * Math.sign(translated[0] - initialPoint.x);
const cy = initialPoint.y + ry * Math.sign(translated[1] - initialPoint.y);
this.drawInstance.center(cx, cy);
this.drawInstance.radius(rx, ry);
this.shapeSizeElement.update(this.drawInstance);
}
});
}
private drawBoxBy4Points(): void {
let numberOfPoints = 0;
this.drawInstance = (this.canvas as any)
@ -396,19 +494,18 @@ export class DrawHandlerImpl implements DrawHandler {
// finish if numberOfPoints are exactly four
if (numberOfPoints === 4) {
const bbox = (e.target as SVGPolylineElement).getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
const points = [bbox.x, bbox.y, bbox.x + bbox.width, bbox.y + bbox.height];
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(points, true);
const { shapeType, redraw: clientID } = this.drawData;
this.cancel();
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
this.onDrawDone(
{
shapeType,
clientID,
points: [xtl, ytl, xbr, ybr],
},
Date.now() - this.startTimestamp,
);
if (checkConstraint('rectangle', [xtl, ytl, xbr, ybr])) {
this.onDrawDone({
shapeType,
clientID,
points: [xtl, ytl, xbr, ybr],
},
Date.now() - this.startTimestamp);
}
}
})
@ -479,7 +576,7 @@ export class DrawHandlerImpl implements DrawHandler {
}
});
// We need scale just drawn points
// We need to scale points that have been just drawn
this.drawInstance.on('drawstart drawpoint', (e: CustomEvent): void => {
this.transform(this.geometry);
lastDrawnPoint.x = e.detail.event.clientX;
@ -487,38 +584,24 @@ export class DrawHandlerImpl implements DrawHandler {
});
this.drawInstance.on('drawdone', (e: CustomEvent): void => {
const targetPoints = pointsToNumberArray((e.target as SVGElement).getAttribute('points'));
const targetPoints = readPointsFromShape((e.target as any as { instance: SVG.Shape }).instance);
const { shapeType, redraw: clientID } = this.drawData;
const { points, box } = shapeType === 'cuboid' ?
this.getFinalCuboidCoordinates(targetPoints) :
this.getFinalPolyshapeCoordinates(targetPoints);
this.getFinalPolyshapeCoordinates(targetPoints, true);
this.release();
if (this.canceled) return;
if (
shapeType === 'polygon' &&
(box.xbr - box.xtl) * (box.ybr - box.ytl) >= consts.AREA_THRESHOLD &&
points.length >= 3 * 2
) {
this.onDrawDone({ clientID, shapeType, points }, Date.now() - this.startTimestamp);
} else if (
shapeType === 'polyline' &&
(box.xbr - box.xtl >= consts.SIZE_THRESHOLD || box.ybr - box.ytl >= consts.SIZE_THRESHOLD) &&
points.length >= 2 * 2
) {
this.onDrawDone({ clientID, shapeType, points }, Date.now() - this.startTimestamp);
} else if (shapeType === 'points' && (e.target as any).getAttribute('points') !== '0,0') {
if (checkConstraint(shapeType, points, box)) {
if (shapeType === 'cuboid') {
this.onDrawDone(
{ clientID, shapeType, points: cuboidFrom4Points(points) },
Date.now() - this.startTimestamp,
);
return;
}
this.onDrawDone({ clientID, shapeType, points }, Date.now() - this.startTimestamp);
// TODO: think about correct constraign for cuboids
} else if (shapeType === 'cuboid' && points.length === 4 * 2) {
this.onDrawDone(
{
clientID,
shapeType,
points: cuboidFrom4Points(points),
},
Date.now() - this.startTimestamp,
);
}
});
}
@ -576,22 +659,20 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawInstance = this.canvas.rect();
this.drawInstance
.on('drawstop', (e: Event): void => {
const bbox = (e.target as SVGRectElement).getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
const points = readPointsFromShape((e.target as any as { instance: SVG.Rect }).instance);
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(points, true);
const { shapeType, redraw: clientID } = this.drawData;
this.release();
if (this.canceled) return;
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
if (checkConstraint('cuboid', [xtl, ytl, xbr, ybr])) {
const d = { x: (xbr - xtl) * 0.1, y: (ybr - ytl) * 0.1 };
this.onDrawDone(
{
shapeType,
points: cuboidFrom4Points([xtl, ybr, xbr, ybr, xbr, ytl, xbr + d.x, ytl - d.y]),
clientID,
},
Date.now() - this.startTimestamp,
);
this.onDrawDone({
shapeType,
points: cuboidFrom4Points([xtl, ybr, xbr, ybr, xbr, ytl, xbr + d.x, ytl - d.y]),
clientID,
},
Date.now() - this.startTimestamp);
}
})
.on('drawupdate', (): void => {
@ -611,27 +692,30 @@ export class DrawHandlerImpl implements DrawHandler {
.split(/[,\s]/g)
.map((coord: string): number => +coord);
const { points } = this.drawData.initialState.shapeType === 'cuboid' ?
const { shapeType } = this.drawData.initialState;
const { points, box } = shapeType === 'cuboid' ?
this.getFinalCuboidCoordinates(targetPoints) :
this.getFinalPolyshapeCoordinates(targetPoints);
this.getFinalPolyshapeCoordinates(targetPoints, true);
if (!e.detail.originalEvent.ctrlKey) {
this.release();
}
this.onDrawDone(
{
shapeType: this.drawData.initialState.shapeType,
objectType: this.drawData.initialState.objectType,
points,
occluded: this.drawData.initialState.occluded,
attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label,
color: this.drawData.initialState.color,
},
Date.now() - this.startTimestamp,
e.detail.originalEvent.ctrlKey,
);
if (checkConstraint(shapeType, points, box)) {
this.onDrawDone(
{
shapeType,
objectType: this.drawData.initialState.objectType,
points,
occluded: this.drawData.initialState.occluded,
attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label,
color: this.drawData.initialState.color,
},
Date.now() - this.startTimestamp,
e.detail.originalEvent.ctrlKey,
);
}
});
}
@ -639,7 +723,10 @@ export class DrawHandlerImpl implements DrawHandler {
private pasteShape(): void {
function moveShape(shape: SVG.Shape, x: number, y: number): void {
const bbox = shape.bbox();
const { rotation } = shape.transform();
shape.untransform();
shape.move(x - bbox.width / 2, y - bbox.height / 2);
shape.rotate(rotation);
}
const { x: initialX, y: initialY } = this.cursorPosition;
@ -651,7 +738,7 @@ export class DrawHandlerImpl implements DrawHandler {
});
}
private pasteBox(box: BBox): void {
private pasteBox(box: BBox, rotation: number): void {
this.drawInstance = (this.canvas as any)
.rect(box.width, box.height)
.move(box.x, box.y)
@ -659,29 +746,71 @@ export class DrawHandlerImpl implements DrawHandler {
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity,
});
}).rotate(rotation);
this.pasteShape();
this.drawInstance.on('done', (e: CustomEvent): void => {
const bbox = this.drawInstance.node.getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
const points = readPointsFromShape((e.target as any as { instance: SVG.Rect }).instance);
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(points, !this.drawData.initialState.rotation);
if (!e.detail.originalEvent.ctrlKey) {
this.release();
}
this.onDrawDone(
{
shapeType: this.drawData.initialState.shapeType,
objectType: this.drawData.initialState.objectType,
points: [xtl, ytl, xbr, ybr],
occluded: this.drawData.initialState.occluded,
attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label,
color: this.drawData.initialState.color,
},
Date.now() - this.startTimestamp,
e.detail.originalEvent.ctrlKey,
if (checkConstraint('rectangle', [xtl, ytl, xbr, ybr])) {
this.onDrawDone(
{
shapeType: this.drawData.initialState.shapeType,
objectType: this.drawData.initialState.objectType,
points: [xtl, ytl, xbr, ybr],
occluded: this.drawData.initialState.occluded,
attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label,
color: this.drawData.initialState.color,
rotation: this.drawData.initialState.rotation,
},
Date.now() - this.startTimestamp,
e.detail.originalEvent.ctrlKey,
);
}
});
}
private pasteEllipse([cx, cy, rx, ry]: number[], rotation: number): void {
this.drawInstance = (this.canvas as any)
.ellipse(rx * 2, ry * 2)
.center(cx, cy)
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity,
}).rotate(rotation);
this.pasteShape();
this.drawInstance.on('done', (e: CustomEvent): void => {
const points = this.getFinalEllipseCoordinates(
readPointsFromShape((e.target as any as { instance: SVG.Ellipse }).instance), false,
);
if (!e.detail.originalEvent.ctrlKey) {
this.release();
}
if (checkConstraint('ellipse', points)) {
this.onDrawDone(
{
shapeType: this.drawData.initialState.shapeType,
objectType: this.drawData.initialState.objectType,
points,
occluded: this.drawData.initialState.occluded,
attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label,
color: this.drawData.initialState.color,
rotation: this.drawData.initialState.rotation,
},
Date.now() - this.startTimestamp,
e.detail.originalEvent.ctrlKey,
);
}
});
}
@ -799,7 +928,13 @@ export class DrawHandlerImpl implements DrawHandler {
y: ytl,
width: xbr - xtl,
height: ybr - ytl,
});
}, this.drawData.initialState.rotation);
} else if (this.drawData.shapeType === 'ellipse') {
const [cx, cy, rightX, topY] = this.drawData.initialState.points.map(
(coord: number): number => coord + offset,
);
this.pasteEllipse([cx, cy, rightX - cx, cy - topY], this.drawData.initialState.rotation);
} else {
const points = this.drawData.initialState.points.map((coord: number): number => coord + offset);
const stringifiedPoints = stringifyPoints(points);
@ -818,12 +953,10 @@ export class DrawHandlerImpl implements DrawHandler {
} else {
if (this.drawData.shapeType === 'rectangle') {
if (this.drawData.rectDrawingMethod === RectDrawingMethod.EXTREME_POINTS) {
// draw box by extreme clicking
this.drawBoxBy4Points();
this.drawBoxBy4Points(); // draw box by extreme clicking
} else {
// default box drawing
this.drawBox();
// Draw instance was initialized after drawBox();
this.drawBox(); // default box drawing
// draw instance was initialized after drawBox();
this.shapeSizeElement = displayShapeSize(this.canvas, this.text);
}
} else if (this.drawData.shapeType === 'polygon') {
@ -832,6 +965,9 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawPolyline();
} else if (this.drawData.shapeType === 'points') {
this.drawPoints();
} else if (this.drawData.shapeType === 'ellipse') {
this.drawEllipse();
this.shapeSizeElement = displayShapeSize(this.canvas, this.text);
} else if (this.drawData.shapeType === 'cuboid') {
if (this.drawData.cuboidDrawingMethod === CuboidDrawingMethod.CORNER_POINTS) {
this.drawCuboidBy4Points();
@ -840,7 +976,10 @@ export class DrawHandlerImpl implements DrawHandler {
this.shapeSizeElement = displayShapeSize(this.canvas, this.text);
}
}
this.setupDrawEvents();
if (this.drawData.shapeType !== 'ellipse') {
this.setupDrawEvents();
}
}
this.startTimestamp = Date.now();
@ -900,7 +1039,7 @@ export class DrawHandlerImpl implements DrawHandler {
if (typeof configuration.autoborders === 'boolean') {
this.autobordersEnabled = configuration.autoborders;
if (this.drawInstance) {
if (this.drawInstance && !this.drawData.initialState) {
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.drawInstance, this.drawData.redraw);
} else {
@ -913,7 +1052,7 @@ export class DrawHandlerImpl implements DrawHandler {
public transform(geometry: Geometry): void {
this.geometry = geometry;
if (this.shapeSizeElement && this.drawInstance && this.drawData.shapeType === 'rectangle') {
if (this.shapeSizeElement && this.drawInstance && ['rectangle', 'ellipse'].includes(this.drawData.shapeType)) {
this.shapeSizeElement.update(this.drawInstance);
}

@ -17,38 +17,25 @@ export interface InteractionHandler {
transform(geometry: Geometry): void;
interact(interactData: InteractionData): void;
configurate(config: Configuration): void;
destroy(): void;
cancel(): void;
}
export class InteractionHandlerImpl implements InteractionHandler {
private onInteraction: (shapes: InteractionResult[] | null, shapesUpdated?: boolean, isDone?: boolean) => void;
private configuration: Configuration;
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 threshold: SVG.Rect | null;
private thresholdRectSize: number;
private intermediateShape: PropType<InteractionData, 'intermediateShape'>;
private drawnIntermediateShape: SVG.Shape;
private thresholdWasModified: boolean;
private prepareResult(): InteractionResult[] {
@ -375,6 +362,27 @@ export class InteractionHandlerImpl implements InteractionHandler {
return false;
}
private onKeyUp = (e: KeyboardEvent): void => {
if (this.interactionData.enabled && e.keyCode === 17) {
if (this.interactionData.onChangeToolsBlockerState && !this.thresholdWasModified) {
this.interactionData.onChangeToolsBlockerState('keyup');
}
if (this.shouldRaiseEvent(false)) {
// 17 is ctrl
this.onInteraction(this.prepareResult(), true, false);
}
}
};
private onKeyDown = (e: KeyboardEvent): void => {
if (!e.repeat && this.interactionData.enabled && e.keyCode === 17) {
if (this.interactionData.onChangeToolsBlockerState && !this.thresholdWasModified) {
this.interactionData.onChangeToolsBlockerState('keydown');
}
this.thresholdWasModified = false;
}
};
public constructor(
onInteraction: (
shapes: InteractionResult[] | null,
@ -452,26 +460,8 @@ export class InteractionHandlerImpl implements InteractionHandler {
}
});
window.addEventListener('keyup', (e: KeyboardEvent): void => {
if (this.interactionData.enabled && e.keyCode === 17) {
if (this.interactionData.onChangeToolsBlockerState && !this.thresholdWasModified) {
this.interactionData.onChangeToolsBlockerState('keyup');
}
if (this.shouldRaiseEvent(false)) {
// 17 is ctrl
this.onInteraction(this.prepareResult(), true, false);
}
}
});
window.addEventListener('keydown', (e: KeyboardEvent): void => {
if (!e.repeat && this.interactionData.enabled && e.keyCode === 17) {
if (this.interactionData.onChangeToolsBlockerState && !this.thresholdWasModified) {
this.interactionData.onChangeToolsBlockerState('keydown');
}
this.thresholdWasModified = false;
}
});
window.document.addEventListener('keyup', this.onKeyUp);
window.document.addEventListener('keydown', this.onKeyDown);
}
public transform(geometry: Geometry): void {
@ -552,4 +542,9 @@ export class InteractionHandlerImpl implements InteractionHandler {
this.release();
this.onInteraction(null);
}
public destroy(): void {
window.document.removeEventListener('keyup', this.onKeyUp);
window.document.removeEventListener('keydown', this.onKeyDown);
}
}

@ -1,4 +1,4 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -44,6 +44,7 @@ export interface DrawnState {
source: 'AUTO' | 'MANUAL';
shapeType: string;
points?: number[];
rotation: number;
attributes: Record<number, string>;
descriptions: string[];
zOrder?: number;
@ -95,16 +96,30 @@ export function displayShapeSize(shapesContainer: SVG.Container, textContainer:
.fill('white')
.addClass('cvat_canvas_text'),
update(shape: SVG.Shape): void {
const bbox = shape.bbox();
const text = `${bbox.width.toFixed(1)}x${bbox.height.toFixed(1)}`;
const [x, y]: number[] = translateToSVG(
let text = `${Math.round(shape.width())}x${Math.round(shape.height())}px`;
if (shape.type === 'rect' || shape.type === 'ellipse') {
let rotation = shape.transform().rotation || 0;
// be sure, that rotation in range [0; 360]
while (rotation < 0) rotation += 360;
rotation %= 360;
if (rotation) {
text = `${text} ${rotation.toFixed(1)}\u00B0`;
}
}
const [x, y, cx, cy]: number[] = translateToSVG(
(textContainer.node as any) as SVGSVGElement,
translateFromSVG((shapesContainer.node as any) as SVGSVGElement, [bbox.x, bbox.y]),
);
translateFromSVG((shapesContainer.node as any) as SVGSVGElement, [
shape.x(),
shape.y(),
shape.cx(),
shape.cy(),
]),
).map((coord: number): number => Math.round(coord));
this.sizeElement
.clear()
.plain(text)
.move(x + consts.TEXT_MARGIN, y + consts.TEXT_MARGIN);
.move(x + consts.TEXT_MARGIN, y + consts.TEXT_MARGIN)
.rotate(shape.transform().rotation, cx, cy);
},
rm(): void {
if (this.sizeElement) {
@ -117,6 +132,23 @@ export function displayShapeSize(shapesContainer: SVG.Container, textContainer:
return shapeSize;
}
export function rotate2DPoints(cx: number, cy: number, angle: number, points: number[]): number[] {
const rad = (Math.PI / 180) * angle;
const cos = Math.cos(rad);
const sin = Math.sin(rad);
const result = [];
for (let i = 0; i < points.length; i += 2) {
const x = points[i];
const y = points[i + 1];
result.push(
(x - cx) * cos - (y - cy) * sin + cx,
(y - cy) * cos + (x - cx) * sin + cy,
);
}
return result;
}
export function pointsToNumberArray(points: string | Point[]): number[] {
if (Array.isArray(points)) {
return points.reduce((acc: number[], point: Point): number[] => {
@ -156,6 +188,22 @@ export function parsePoints(source: string | number[]): Point[] {
);
}
export function readPointsFromShape(shape: SVG.Shape): number[] {
let points = null;
if (shape.type === 'ellipse') {
const [rx, ry] = [+shape.attr('rx'), +shape.attr('ry')];
const [cx, cy] = [+shape.attr('cx'), +shape.attr('cy')];
points = `${cx},${cy} ${cx + rx},${cy - ry}`;
} else if (shape.type === 'rect') {
points = `${shape.attr('x')},${shape.attr('y')} ` +
`${shape.attr('x') + shape.attr('width')},${shape.attr('y') + shape.attr('height')}`;
} else {
points = shape.attr('points');
}
return pointsToNumberArray(points);
}
export function stringifyPoints(points: (Point | number)[]): string {
if (typeof points[0] === 'number') {
return points.reduce((acc: string, val: number, idx: number): string => {
@ -187,4 +235,8 @@ export function translateToCanvas(offset: number, points: number[]): number[] {
return points.map((coord: number): number => coord + offset);
}
export function translateFromCanvas(offset: number, points: number[]): number[] {
return points.map((coord: number): number => coord - offset);
}
export type PropType<T, Prop extends keyof T> = T[Prop];

@ -167,18 +167,23 @@ SVG.Element.prototype.resize = function constructor(...args: any): any {
handler = this.remember('_resizeHandler');
handler.resize = function (e: any) {
const { event } = e.detail;
this.rotationPointPressed = e.type === 'rot';
if (
event.button === 0 &&
// ignore shift key for cuboid change perspective
(!event.shiftKey || this.el.parent().hasClass('cvat_canvas_shape_cuboid')) &&
!event.altKey
// ignore shift key for cuboids (change perspective) and rectangles (precise rotation)
(!event.shiftKey || (
this.el.parent().hasClass('cvat_canvas_shape_cuboid')
|| this.el.type === 'rect')
) && !event.altKey
) {
return handler.constructor.prototype.resize.call(this, e);
}
};
handler.update = function (e: any) {
this.m = this.el.node.getScreenCTM().inverse();
return handler.constructor.prototype.update.call(this, e);
if (!this.rotationPointPressed) {
this.m = this.el.node.getScreenCTM().inverse();
}
handler.constructor.prototype.update.call(this, e);
};
} else {
originalResize.call(this, ...args);

@ -36,6 +36,7 @@ interface Canvas3d {
fitCanvas(): void;
fit(): void;
group(groupData: GroupData): void;
destroy(): void;
}
class Canvas3dImpl implements Canvas3d {
@ -104,6 +105,10 @@ class Canvas3dImpl implements Canvas3d {
public fitCanvas(): void {
this.model.fit();
}
public destroy(): void {
this.model.destroy();
}
}
export {

@ -126,6 +126,7 @@ export interface Canvas3dModel {
configureShapes(shapeProperties: any): void;
fit(): void;
group(groupData: GroupData): void;
destroy(): void;
}
export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
@ -234,8 +235,8 @@ export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
}
public isAbleToChangeFrame(): boolean {
const isUnable = [Mode.DRAG, Mode.EDIT, Mode.RESIZE, Mode.INTERACT, Mode.BUSY].includes(this.data.mode)
|| (this.data.mode === Mode.DRAW && typeof this.data.drawData.redraw === 'number');
const isUnable = [Mode.DRAG, Mode.EDIT, Mode.RESIZE, Mode.INTERACT, Mode.BUSY].includes(this.data.mode) ||
(this.data.mode === Mode.DRAW && typeof this.data.drawData.redraw === 'number');
return !isUnable;
}
@ -340,4 +341,6 @@ export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
public get groupData(): GroupData {
return { ...this.data.groupData };
}
public destroy(): void {}
}

@ -287,6 +287,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
(_state: any): boolean => _state.clientID === Number(intersects[0].object.name),
);
if (item.length !== 0) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.model.data.groupData.grouped = this.model.data.groupData.grouped.filter(
(_state: any): boolean => _state.clientID !== Number(intersects[0].object.name),
@ -543,9 +544,9 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
this.action.rotation.screenInit = { x: diffX, y: diffY };
this.action.rotation.screenMove = { x: diffX, y: diffY };
if (
this.model.data.selected
&& !this.model.data.selected.perspective.userData.lock
&& !this.model.data.selected.perspective.userData.hidden
this.model.data.selected &&
!this.model.data.selected.perspective.userData.lock &&
!this.model.data.selected.perspective.userData.hidden
) {
this.action.scan = view;
this.model.mode = Mode.EDIT;
@ -698,8 +699,8 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
cuboid.setOpacity(opacity);
if (
this.model.data.activeElement.clientID === clientID
&& ![Mode.DRAG_CANVAS, Mode.GROUP].includes(this.mode)
this.model.data.activeElement.clientID === clientID &&
![Mode.DRAG_CANVAS, Mode.GROUP].includes(this.mode)
) {
cuboid.setOpacity(selectedOpacity);
if (!object.lock) {
@ -964,12 +965,12 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
const sphereCenter = points.geometry.boundingSphere.center;
const { radius } = points.geometry.boundingSphere;
if (!this.views.perspective.camera) return;
const xRange = -radius / 2 < this.views.perspective.camera.position.x - sphereCenter.x
&& radius / 2 > this.views.perspective.camera.position.x - sphereCenter.x;
const yRange = -radius / 2 < this.views.perspective.camera.position.y - sphereCenter.y
&& radius / 2 > this.views.perspective.camera.position.y - sphereCenter.y;
const zRange = -radius / 2 < this.views.perspective.camera.position.z - sphereCenter.z
&& radius / 2 > this.views.perspective.camera.position.z - sphereCenter.z;
const xRange = -radius / 2 < this.views.perspective.camera.position.x - sphereCenter.x &&
radius / 2 > this.views.perspective.camera.position.x - sphereCenter.x;
const yRange = -radius / 2 < this.views.perspective.camera.position.y - sphereCenter.y &&
radius / 2 > this.views.perspective.camera.position.y - sphereCenter.y;
const zRange = -radius / 2 < this.views.perspective.camera.position.z - sphereCenter.z &&
radius / 2 > this.views.perspective.camera.position.z - sphereCenter.z;
let newX = 0;
let newY = 0;
let newZ = 0;
@ -1085,10 +1086,10 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
private positionAllViews(x: number, y: number, z: number, animation: boolean): void {
if (
this.views.perspective.controls
&& this.views.top.controls
&& this.views.side.controls
&& this.views.front.controls
this.views.perspective.controls &&
this.views.top.controls &&
this.views.side.controls &&
this.views.front.controls
) {
this.views.perspective.controls.setLookAt(x - 8, y - 8, z + 3, x, y, z, animation);
this.views.top.camera.position.set(x, y, z + 8);
@ -1266,8 +1267,8 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
private renderTranslateAction(view: ViewType, viewType: any): void {
if (
this.action.translation.helper.x === this.views[view].rayCaster.mouseVector.x
&& this.action.translation.helper.y === this.views[view].rayCaster.mouseVector.y
this.action.translation.helper.x === this.views[view].rayCaster.mouseVector.x &&
this.action.translation.helper.y === this.views[view].rayCaster.mouseVector.y
) {
return;
}
@ -1332,8 +1333,8 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
}
if (
this.action.resize.recentMouseVector.x === currentPosX
&& this.action.resize.recentMouseVector.y === currentPosY
this.action.resize.recentMouseVector.x === currentPosX &&
this.action.resize.recentMouseVector.y === currentPosY
) {
return;
}
@ -1736,15 +1737,15 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
y: canvas.offsetTop + canvas.offsetHeight / 2,
};
if (
this.action.rotation.screenInit.x === this.action.rotation.screenMove.x
&& this.action.rotation.screenInit.y === this.action.rotation.screenMove.y
this.action.rotation.screenInit.x === this.action.rotation.screenMove.x &&
this.action.rotation.screenInit.y === this.action.rotation.screenMove.y
) {
return;
}
if (
this.action.rotation.recentMouseVector.x === this.views[view].rayCaster.mouseVector.x
&& this.action.rotation.recentMouseVector.y === this.views[view].rayCaster.mouseVector.y
this.action.rotation.recentMouseVector.x === this.views[view].rayCaster.mouseVector.x &&
this.action.rotation.recentMouseVector.y === this.views[view].rayCaster.mouseVector.y
) {
return;
}

@ -1,18 +1,18 @@
{
"name": "cvat-core",
"version": "3.17.0",
"version": "4.2.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "cvat-core",
"version": "3.17.0",
"version": "4.2.1",
"license": "MIT",
"dependencies": {
"axios": "^0.21.4",
"browser-or-node": "^1.2.1",
"cvat-data": "../cvat-data",
"detect-browser": "^5.2.0",
"detect-browser": "^5.2.1",
"error-stack-parser": "^2.0.2",
"form-data": "^2.5.0",
"jest-config": "^26.6.3",
@ -20,7 +20,8 @@
"json-logic-js": "^2.0.1",
"platform": "^1.3.5",
"quickhull": "^1.0.3",
"store": "^2.0.12"
"store": "^2.0.12",
"tus-js-client": "^2.3.0"
},
"devDependencies": {
"coveralls": "^3.0.5",
@ -38,6 +39,12 @@
},
"devDependencies": {}
},
"detect-browser": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/detect-browser/-/detect-browser-5.2.1.tgz",
"integrity": "sha512-eAcRiEPTs7utXWPaAgu/OX1HRJpxW7xSHpw4LTDrGFaeWnJ37HRlqpUkKsDm0AoTbtrvHQhH+5U2Cd87EGhJTg==",
"extraneous": true
},
"node_modules/@babel/code-frame": {
"version": "7.15.8",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz",
@ -1617,6 +1624,15 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/combine-errors": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/combine-errors/-/combine-errors-3.0.3.tgz",
"integrity": "sha1-9N9nQAg+VwOjGBEQwrEFUfAD2oY=",
"dependencies": {
"custom-error-instance": "2.1.1",
"lodash.uniqby": "4.5.0"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -1714,6 +1730,11 @@
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="
},
"node_modules/custom-error-instance": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/custom-error-instance/-/custom-error-instance-2.1.1.tgz",
"integrity": "sha1-PPY5FIemYppiR+sMoM4ACBt+Nho="
},
"node_modules/cvat-data": {
"resolved": "../cvat-data",
"link": true
@ -2279,9 +2300,9 @@
}
},
"node_modules/follow-redirects": {
"version": "1.14.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz",
"integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==",
"version": "1.14.8",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==",
"funding": [
{
"type": "individual",
@ -2814,7 +2835,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"dev": true,
"engines": {
"node": ">=8"
},
@ -3669,6 +3689,11 @@
"node": ">= 10.13.0"
}
},
"node_modules/js-base64": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz",
"integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ=="
},
"node_modules/js-cookie": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
@ -3957,6 +3982,60 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash._baseiteratee": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash._baseiteratee/-/lodash._baseiteratee-4.7.0.tgz",
"integrity": "sha1-NKm1VDVycnw9sueO2uPA6eZr0QI=",
"dependencies": {
"lodash._stringtopath": "~4.8.0"
}
},
"node_modules/lodash._basetostring": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-4.12.0.tgz",
"integrity": "sha1-kyfJ3FFYhmt/pLnUL0Y45XZt2d8="
},
"node_modules/lodash._baseuniq": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz",
"integrity": "sha1-DrtE5FaBSveQXGIS+iybLVG4Qeg=",
"dependencies": {
"lodash._createset": "~4.0.0",
"lodash._root": "~3.0.0"
}
},
"node_modules/lodash._createset": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/lodash._createset/-/lodash._createset-4.0.3.tgz",
"integrity": "sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY="
},
"node_modules/lodash._root": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz",
"integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI="
},
"node_modules/lodash._stringtopath": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/lodash._stringtopath/-/lodash._stringtopath-4.8.0.tgz",
"integrity": "sha1-lBvPDmQmbl/B1m/tCmlZVExXaCQ=",
"dependencies": {
"lodash._basetostring": "~4.12.0"
}
},
"node_modules/lodash.throttle": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
"integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
},
"node_modules/lodash.uniqby": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.5.0.tgz",
"integrity": "sha1-o6F7v2LutiQPSRhG6XwcTipeHiE=",
"dependencies": {
"lodash._baseiteratee": "~4.7.0",
"lodash._baseuniq": "~4.6.0"
}
},
"node_modules/log-driver": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz",
@ -4630,6 +4709,18 @@
"node": ">= 6"
}
},
"node_modules/proper-lockfile": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-2.0.1.tgz",
"integrity": "sha1-FZ+wYZPTIAP0s2kd0uwaY0qoDR0=",
"dependencies": {
"graceful-fs": "^4.1.2",
"retry": "^0.10.0"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/psl": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
@ -4661,6 +4752,11 @@
"node": ">=0.6"
}
},
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
},
"node_modules/quickhull": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/quickhull/-/quickhull-1.0.3.tgz",
@ -4832,6 +4928,11 @@
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
},
"node_modules/requizzle": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.3.tgz",
@ -4887,6 +4988,14 @@
"node": ">=0.12"
}
},
"node_modules/retry": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz",
"integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=",
"engines": {
"node": "*"
}
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@ -5189,11 +5298,6 @@
"node": ">=0.10.0"
}
},
"detect-browser": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/detect-browser/-/detect-browser-5.2.1.tgz",
"integrity": "sha512-eAcRiEPTs7utXWPaAgu/OX1HRJpxW7xSHpw4LTDrGFaeWnJ37HRlqpUkKsDm0AoTbtrvHQhH+5U2Cd87EGhJTg=="
},
"node_modules/saxes": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
@ -5932,6 +6036,25 @@
"node": "*"
}
},
"node_modules/tus-js-client": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tus-js-client/-/tus-js-client-2.3.0.tgz",
"integrity": "sha512-I4cSwm6N5qxqCmBqenvutwSHe9ntf81lLrtf6BmLpG2v4wTl89atCQKqGgqvkodE6Lx+iKIjMbaXmfvStTg01g==",
"dependencies": {
"buffer-from": "^0.1.1",
"combine-errors": "^3.0.3",
"is-stream": "^2.0.0",
"js-base64": "^2.6.1",
"lodash.throttle": "^4.1.1",
"proper-lockfile": "^2.0.1",
"url-parse": "^1.4.3"
}
},
"node_modules/tus-js-client/node_modules/buffer-from": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-0.1.2.tgz",
"integrity": "sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg=="
},
"node_modules/tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
@ -6078,6 +6201,15 @@
"integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
"deprecated": "Please see https://github.com/lydell/urix#deprecated"
},
"node_modules/url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"dependencies": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"node_modules/use": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
@ -6207,7 +6339,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"dependencies": {
"isexe": "^2.0.0"
},
@ -7581,6 +7712,15 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"combine-errors": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/combine-errors/-/combine-errors-3.0.3.tgz",
"integrity": "sha1-9N9nQAg+VwOjGBEQwrEFUfAD2oY=",
"requires": {
"custom-error-instance": "2.1.1",
"lodash.uniqby": "4.5.0"
}
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -7662,6 +7802,11 @@
}
}
},
"custom-error-instance": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/custom-error-instance/-/custom-error-instance-2.1.1.tgz",
"integrity": "sha1-PPY5FIemYppiR+sMoM4ACBt+Nho="
},
"cvat-data": {
"version": "file:../cvat-data",
"requires": {
@ -8099,9 +8244,9 @@
}
},
"follow-redirects": {
"version": "1.14.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz",
"integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g=="
"version": "1.14.8",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA=="
},
"for-in": {
"version": "1.0.2",
@ -8482,8 +8627,7 @@
"is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"dev": true
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="
},
"is-typedarray": {
"version": "1.0.0",
@ -9155,6 +9299,11 @@
"supports-color": "^7.0.0"
}
},
"js-base64": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz",
"integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ=="
},
"js-cookie": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
@ -9385,6 +9534,60 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash._baseiteratee": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash._baseiteratee/-/lodash._baseiteratee-4.7.0.tgz",
"integrity": "sha1-NKm1VDVycnw9sueO2uPA6eZr0QI=",
"requires": {
"lodash._stringtopath": "~4.8.0"
}
},
"lodash._basetostring": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-4.12.0.tgz",
"integrity": "sha1-kyfJ3FFYhmt/pLnUL0Y45XZt2d8="
},
"lodash._baseuniq": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz",
"integrity": "sha1-DrtE5FaBSveQXGIS+iybLVG4Qeg=",
"requires": {
"lodash._createset": "~4.0.0",
"lodash._root": "~3.0.0"
}
},
"lodash._createset": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/lodash._createset/-/lodash._createset-4.0.3.tgz",
"integrity": "sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY="
},
"lodash._root": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz",
"integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI="
},
"lodash._stringtopath": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/lodash._stringtopath/-/lodash._stringtopath-4.8.0.tgz",
"integrity": "sha1-lBvPDmQmbl/B1m/tCmlZVExXaCQ=",
"requires": {
"lodash._basetostring": "~4.12.0"
}
},
"lodash.throttle": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
"integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
},
"lodash.uniqby": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.5.0.tgz",
"integrity": "sha1-o6F7v2LutiQPSRhG6XwcTipeHiE=",
"requires": {
"lodash._baseiteratee": "~4.7.0",
"lodash._baseuniq": "~4.6.0"
}
},
"log-driver": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz",
@ -9893,6 +10096,15 @@
"sisteransi": "^1.0.5"
}
},
"proper-lockfile": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-2.0.1.tgz",
"integrity": "sha1-FZ+wYZPTIAP0s2kd0uwaY0qoDR0=",
"requires": {
"graceful-fs": "^4.1.2",
"retry": "^0.10.0"
}
},
"psl": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
@ -9918,6 +10130,11 @@
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
"dev": true
},
"querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
},
"quickhull": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/quickhull/-/quickhull-1.0.3.tgz",
@ -10054,6 +10271,11 @@
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
},
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
},
"requizzle": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.3.tgz",
@ -10096,6 +10318,11 @@
"resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
"integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="
},
"retry": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz",
"integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q="
},
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@ -10330,14 +10557,6 @@
"is-number": "^3.0.0",
"repeat-string": "^1.6.1"
}
},
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
"requires": {
"isexe": "^2.0.0"
}
}
}
},
@ -10923,6 +11142,27 @@
"safe-buffer": "^5.0.1"
}
},
"tus-js-client": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tus-js-client/-/tus-js-client-2.3.0.tgz",
"integrity": "sha512-I4cSwm6N5qxqCmBqenvutwSHe9ntf81lLrtf6BmLpG2v4wTl89atCQKqGgqvkodE6Lx+iKIjMbaXmfvStTg01g==",
"requires": {
"buffer-from": "^0.1.1",
"combine-errors": "^3.0.3",
"is-stream": "^2.0.0",
"js-base64": "^2.6.1",
"lodash.throttle": "^4.1.1",
"proper-lockfile": "^2.0.1",
"url-parse": "^1.4.3"
},
"dependencies": {
"buffer-from": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-0.1.2.tgz",
"integrity": "sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg=="
}
}
},
"tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
@ -11041,6 +11281,15 @@
"resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
"integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI="
},
"url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"requires": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"use": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
@ -11148,7 +11397,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"requires": {
"isexe": "^2.0.0"
}

@ -1,6 +1,6 @@
{
"name": "cvat-core",
"version": "3.17.0",
"version": "4.2.1",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js",
"scripts": {
@ -31,10 +31,11 @@
"error-stack-parser": "^2.0.2",
"form-data": "^2.5.0",
"jest-config": "^26.6.3",
"json-logic-js": "^2.0.1",
"js-cookie": "^2.2.0",
"json-logic-js": "^2.0.1",
"platform": "^1.3.5",
"quickhull": "^1.0.3",
"store": "^2.0.12"
"store": "^2.0.12",
"tus-js-client": "^2.3.0"
}
}

@ -1,4 +1,4 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -8,11 +8,13 @@
PolygonShape,
PolylineShape,
PointsShape,
EllipseShape,
CuboidShape,
RectangleTrack,
PolygonTrack,
PolylineTrack,
PointsTrack,
EllipseTrack,
CuboidTrack,
Track,
Shape,
@ -48,6 +50,9 @@
case 'points':
shapeModel = new PointsShape(shapeData, clientID, color, injection);
break;
case 'ellipse':
shapeModel = new EllipseShape(shapeData, clientID, color, injection);
break;
case 'cuboid':
shapeModel = new CuboidShape(shapeData, clientID, color, injection);
break;
@ -77,6 +82,9 @@
case 'points':
trackModel = new PointsTrack(trackData, clientID, color, injection);
break;
case 'ellipse':
trackModel = new EllipseTrack(trackData, clientID, color, injection);
break;
case 'cuboid':
trackModel = new CuboidTrack(trackData, clientID, color, injection);
break;
@ -235,7 +243,7 @@
const object = this.objects[state.clientID];
if (typeof object === 'undefined') {
throw new ArgumentError(
'The object has not been saved yet. Call ObjectState.put([state]) before you can merge it',
'The object is not in collection yet. Call ObjectState.put([state]) before you can merge it',
);
}
return object;
@ -282,6 +290,7 @@
frame: object.frame,
points: [...object.points],
occluded: object.occluded,
rotation: object.rotation,
zOrder: object.zOrder,
outside: false,
attributes: Object.keys(object.attributes).reduce((accumulator, attrID) => {
@ -333,6 +342,7 @@
type: shapeType,
frame: +keyframe,
points: [...shape.points],
rotation: shape.rotation,
occluded: shape.occluded,
outside: shape.outside,
zOrder: shape.zOrder,
@ -442,6 +452,7 @@
const position = {
type: objectState.shapeType,
points: [...objectState.points],
rotation: objectState.rotation,
occluded: objectState.occluded,
outside: objectState.outside,
zOrder: objectState.zOrder,
@ -481,6 +492,12 @@
return shape;
});
prev.shapes.push(position);
// add extra keyframe if no other keyframes before outside
if (!prev.shapes.some((shape) => shape.frame === frame - 1)) {
prev.shapes.push(JSON.parse(JSON.stringify(position)));
prev.shapes[prev.shapes.length - 2].frame -= 1;
}
prev.shapes[prev.shapes.length - 1].outside = true;
let clientID = ++this.count;
@ -606,6 +623,10 @@
shape: 0,
track: 0,
},
ellipse: {
shape: 0,
track: 0,
},
cuboid: {
shape: 0,
track: 0,
@ -725,6 +746,7 @@
checkObjectType('object state', state, null, ObjectState);
checkObjectType('state client ID', state.clientID, 'undefined', null);
checkObjectType('state frame', state.frame, 'integer', null);
checkObjectType('state rotation', state.rotation || 0, 'number', null);
checkObjectType('state attributes', state.attributes, null, Object);
checkObjectType('state label', state.label, null, Label);
@ -768,6 +790,7 @@
label_id: state.label.id,
occluded: state.occluded || false,
points: [...state.points],
rotation: state.rotation || 0,
type: state.shapeType,
z_order: state.zOrder,
source: state.source,
@ -787,6 +810,7 @@
occluded: state.occluded || false,
outside: false,
points: [...state.points],
rotation: state.rotation || 0,
type: state.shapeType,
z_order: state.zOrder,
},
@ -844,7 +868,7 @@
if (typeof object === 'undefined') {
throw new ArgumentError('The object has not been saved yet. Call annotations.put([state]) before');
}
const distance = object.constructor.distance(state.points, x, y);
const distance = object.constructor.distance(state.points, x, y, state.rotation);
if (distance !== null && (minimumDistance === null || distance < minimumDistance)) {
minimumDistance = distance;
minimumState = state;
@ -893,35 +917,14 @@
search(filters, frameFrom, frameTo) {
const sign = Math.sign(frameTo - frameFrom);
const filtersStr = JSON.stringify(filters);
const containsDifficultProperties = filtersStr.match(/"var":"width"/) || filtersStr.match(/"var":"height"/);
const deepSearch = (deepSearchFrom, deepSearchTo) => {
// deepSearchFrom is expected to be a frame that doesn't satisfy a filter
// deepSearchTo is expected to be a frame that satisfies a filter
let [prev, next] = [deepSearchFrom, deepSearchTo];
// half division method instead of linear search
while (!(Math.abs(prev - next) === 1)) {
const middle = next + Math.floor((prev - next) / 2);
const shapesData = this.tracks.map((track) => track.get(middle));
const filtered = this.annotationsFilter.filter(shapesData, filters);
if (filtered.length) {
next = middle;
} else {
prev = middle;
}
}
const linearSearch = filtersStr.match(/"var":"width"/) || filtersStr.match(/"var":"height"/);
return next;
};
const keyframesMemory = {};
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)) {
// First prepare all data for the frame
// Consider all shapes, tags, and not outside tracks that have keyframe here
// In particular consider first and last frame as keyframes for all frames
// In particular consider first and last frame as keyframes for all tracks
const statesData = [].concat(
(frame in this.shapes ? this.shapes[frame] : [])
.filter((shape) => !shape.removed)
@ -931,7 +934,9 @@
.map((tag) => tag.get(frame)),
);
const tracks = Object.values(this.tracks)
.filter((track) => frame in track.shapes || frame === frameFrom || frame === frameTo)
.filter((track) => (
frame in track.shapes || frame === frameFrom ||
frame === frameTo || linearSearch))
.filter((track) => !track.removed);
statesData.push(...tracks.map((track) => track.get(frame)).filter((state) => !state.outside));
@ -942,31 +947,6 @@
// Filtering
const filtered = this.annotationsFilter.filter(statesData, filters);
// Now we are checking whether we need deep search or not
// Deep search is needed in some difficult cases
// For example when filter contains fields which
// can be changed between keyframes (like: height and width of a shape)
// It's expected, that a track doesn't satisfy a filter on the previous keyframe
// At the same time it sutisfies the filter on the next keyframe
let withDeepSearch = false;
if (containsDifficultProperties) {
for (const track of tracks) {
const trackIsSatisfy = filtered.includes(track.clientID);
if (!trackIsSatisfy) {
keyframesMemory[track.clientID] = [filtered.includes(track.clientID), frame];
} else if (keyframesMemory[track.clientID] && keyframesMemory[track.clientID][0] === false) {
withDeepSearch = true;
}
}
}
if (withDeepSearch) {
const reducer = sign > 0 ? Math.min : Math.max;
const deepSearchFrom = reducer(...Object.values(keyframesMemory).map((value) => value[1]));
return deepSearch(deepSearchFrom, frame);
}
if (filtered.length) {
return frame;
}

@ -47,13 +47,28 @@
}
} else if (shapeType === ObjectShape.CUBOID) {
if (points.length / 2 !== 8) {
throw new DataError(`Points must have exact 8 points, but got ${points.length / 2}`);
throw new DataError(`Cuboid must have 8 points, but got ${points.length / 2}`);
}
} else if (shapeType === ObjectShape.ELLIPSE) {
if (points.length / 2 !== 2) {
throw new DataError(`Ellipse must have 1 point, rx and ry but got ${points.toString()}`);
}
} else {
throw new ArgumentError(`Unknown value of shapeType has been received ${shapeType}`);
}
}
function findAngleDiff(rightAngle, leftAngle) {
let angleDiff = rightAngle - leftAngle;
angleDiff = ((angleDiff + 180) % 360) - 180;
if (Math.abs(angleDiff) >= 180) {
// if the main arc is bigger than 180, go another arc
// to find it, just substract absolute value from 360 and inverse sign
angleDiff = 360 - Math.abs(angleDiff) * Math.sign(angleDiff) * -1;
}
return angleDiff;
}
function checkShapeArea(shapeType, points) {
const MIN_SHAPE_LENGTH = 3;
const MIN_SHAPE_AREA = 9;
@ -62,6 +77,12 @@
return true;
}
if (shapeType === ObjectShape.ELLIPSE) {
const [cx, cy, rightX, topY] = points;
const [rx, ry] = [rightX - cx, cy - topY];
return rx * ry * Math.PI > MIN_SHAPE_AREA;
}
let xmin = Number.MAX_SAFE_INTEGER;
let xmax = Number.MIN_SAFE_INTEGER;
let ymin = Number.MAX_SAFE_INTEGER;
@ -76,7 +97,6 @@
if (shapeType === ObjectShape.POLYLINE) {
const length = Math.max(xmax - xmin, ymax - ymin);
return length >= MIN_SHAPE_LENGTH;
}
@ -84,30 +104,34 @@
return area >= MIN_SHAPE_AREA;
}
function fitPoints(shapeType, points, maxX, maxY) {
const fittedPoints = [];
for (let i = 0; i < points.length - 1; i += 2) {
const x = points[i];
const y = points[i + 1];
function rotatePoint(x, y, angle, cx = 0, cy = 0) {
const sin = Math.sin((angle * Math.PI) / 180);
const cos = Math.cos((angle * Math.PI) / 180);
const rotX = (x - cx) * cos - (y - cy) * sin + cx;
const rotY = (y - cy) * cos + (x - cx) * sin + cy;
return [rotX, rotY];
}
checkObjectType('coordinate', x, 'number', null);
checkObjectType('coordinate', y, 'number', null);
function fitPoints(shapeType, points, rotation, maxX, maxY) {
checkObjectType('rotation', rotation, 'number', null);
points.forEach((coordinate) => checkObjectType('coordinate', coordinate, 'number', null));
fittedPoints.push(Math.clamp(x, 0, maxX), Math.clamp(y, 0, maxY));
if (shapeType === ObjectShape.CUBOID || shapeType === ObjectShape.ELLIPSE || !!rotation) {
// cuboids and rotated bounding boxes cannot be fitted
return points;
}
return shapeType === ObjectShape.CUBOID ? points : fittedPoints;
}
const fittedPoints = [];
function checkOutside(points, width, height) {
let inside = false;
for (let i = 0; i < points.length - 1; i += 2) {
const [x, y] = points.slice(i);
inside = inside || (x >= 0 && x <= width && y >= 0 && y <= height);
const x = points[i];
const y = points[i + 1];
const clampedX = Math.clamp(x, 0, maxX);
const clampedY = Math.clamp(y, 0, maxY);
fittedPoints.push(clampedX, clampedY);
}
return !inside;
return fittedPoints;
}
function validateAttributeValue(value, attr) {
@ -345,13 +369,13 @@
checkNumberOfPoints(this.shapeType, data.points);
// cut points
const { width, height, filename } = this.frameMeta[frame];
fittedPoints = fitPoints(this.shapeType, data.points, width, height);
fittedPoints = fitPoints(this.shapeType, data.points, data.rotation, width, height);
let check = true;
if (filename && filename.slice(filename.length - 3) === 'pcd') {
check = false;
}
if (check) {
if (!checkShapeArea(this.shapeType, fittedPoints) || checkOutside(fittedPoints, width, height)) {
if (!checkShapeArea(this.shapeType, fittedPoints)) {
fittedPoints = [];
}
}
@ -492,6 +516,7 @@
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.points = data.points;
this.rotation = data.rotation || 0;
this.occluded = data.occluded;
this.zOrder = data.z_order;
}
@ -504,6 +529,7 @@
occluded: this.occluded,
z_order: this.zOrder,
points: [...this.points],
rotation: this.rotation,
attributes: Object.keys(this.attributes).reduce((attributeAccumulator, attrId) => {
attributeAccumulator.push({
spec_id: attrId,
@ -535,6 +561,7 @@
lock: this.lock,
zOrder: this.zOrder,
points: [...this.points],
rotation: this.rotation,
attributes: { ...this.attributes },
descriptions: [...this.descriptions],
label: this.label,
@ -548,9 +575,11 @@
};
}
_savePoints(points, frame) {
_savePoints(points, rotation, frame) {
const undoPoints = this.points;
const undoRotation = this.rotation;
const redoPoints = points;
const redoRotation = rotation;
const undoSource = this.source;
const redoSource = Source.MANUAL;
@ -559,11 +588,13 @@
() => {
this.points = undoPoints;
this.source = undoSource;
this.rotation = undoRotation;
this.updated = Date.now();
},
() => {
this.points = redoPoints;
this.source = redoSource;
this.rotation = redoRotation;
this.updated = Date.now();
},
[this.clientID],
@ -572,6 +603,7 @@
this.source = Source.MANUAL;
this.points = points;
this.rotation = rotation;
}
_saveOccluded(occluded, frame) {
@ -637,6 +669,7 @@
const updated = data.updateFlags;
const fittedPoints = this._validateStateBeforeSave(frame, data, updated);
const { rotation } = data;
// Now when all fields are validated, we can apply them
if (updated.label) {
@ -652,7 +685,7 @@
}
if (updated.points && fittedPoints.length) {
this._savePoints(fittedPoints, frame);
this._savePoints(fittedPoints, rotation, frame);
}
if (updated.occluded) {
@ -696,6 +729,7 @@
zOrder: value.z_order,
points: value.points,
outside: value.outside,
rotation: value.rotation || 0,
attributes: value.attributes.reduce((attributeAccumulator, attr) => {
attributeAccumulator[attr.spec_id] = attr.value;
return attributeAccumulator;
@ -736,6 +770,7 @@
occluded: this.shapes[frame].occluded,
z_order: this.shapes[frame].zOrder,
points: [...this.shapes[frame].points],
rotation: this.shapes[frame].rotation,
outside: this.shapes[frame].outside,
attributes: Object.keys(this.shapes[frame].attributes).reduce(
(attributeAccumulator, attrId) => {
@ -1009,22 +1044,21 @@
);
}
_savePoints(points, frame) {
_savePoints(points, rotation, frame) {
const current = this.get(frame);
const wasKeyframe = frame in this.shapes;
const undoSource = this.source;
const redoSource = Source.MANUAL;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = wasKeyframe ?
{ ...this.shapes[frame], points } :
{
frame,
points,
zOrder: current.zOrder,
outside: current.outside,
occluded: current.occluded,
attributes: {},
};
const redoShape = wasKeyframe ? { ...this.shapes[frame], points, rotation } : {
frame,
points,
rotation,
zOrder: current.zOrder,
outside: current.outside,
occluded: current.occluded,
attributes: {},
};
this.shapes[frame] = redoShape;
this.source = Source.MANUAL;
@ -1049,6 +1083,7 @@
{
frame,
outside,
rotation: current.rotation,
zOrder: current.zOrder,
points: current.points,
occluded: current.occluded,
@ -1078,6 +1113,7 @@
{
frame,
occluded,
rotation: current.rotation,
zOrder: current.zOrder,
points: current.points,
outside: current.outside,
@ -1107,6 +1143,7 @@
{
frame,
zOrder,
rotation: current.rotation,
occluded: current.occluded,
points: current.points,
outside: current.outside,
@ -1139,6 +1176,7 @@
const redoShape = keyframe ?
{
frame,
rotation: current.rotation,
zOrder: current.zOrder,
points: current.points,
outside: current.outside,
@ -1172,6 +1210,7 @@
const updated = data.updateFlags;
const fittedPoints = this._validateStateBeforeSave(frame, data, updated);
const { rotation } = data;
if (updated.label) {
this._saveLabel(data.label, frame);
@ -1194,7 +1233,7 @@
}
if (updated.points && fittedPoints.length) {
this._savePoints(fittedPoints, frame);
this._savePoints(fittedPoints, rotation, frame);
}
if (updated.outside) {
@ -1246,6 +1285,7 @@
if (leftPosition) {
return {
points: [...leftPosition.points],
rotation: leftPosition.rotation,
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
@ -1256,10 +1296,11 @@
if (rightPosition) {
return {
points: [...rightPosition.points],
rotation: rightPosition.rotation,
occluded: rightPosition.occluded,
outside: true,
zOrder: rightPosition.zOrder,
keyframe: targetFrame in this.shapes,
outside: true,
};
}
@ -1356,20 +1397,84 @@
checkNumberOfPoints(this.shapeType, this.points);
}
static distance(points, x, y) {
static distance(points, x, y, angle) {
const [xtl, ytl, xbr, ybr] = points;
const cx = xtl + (xbr - xtl) / 2;
const cy = ytl + (ybr - ytl) / 2;
const [rotX, rotY] = rotatePoint(x, y, -angle, cx, cy);
if (!(x >= xtl && x <= xbr && y >= ytl && y <= ybr)) {
if (!(rotX >= xtl && rotX <= xbr && rotY >= ytl && rotY <= ybr)) {
// Cursor is outside of a box
return null;
}
// The shortest distance from point to an edge
return Math.min.apply(null, [x - xtl, y - ytl, xbr - x, ybr - y]);
return Math.min.apply(null, [rotX - xtl, rotY - ytl, xbr - rotX, ybr - rotY]);
}
}
class EllipseShape extends Shape {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shapeType = ObjectShape.ELLIPSE;
this.pinned = false;
checkNumberOfPoints(this.shapeType, this.points);
}
static distance(points, x, y, angle) {
const [cx, cy, rightX, topY] = points;
const [rx, ry] = [rightX - cx, cy - topY];
const [rotX, rotY] = rotatePoint(x, y, -angle, cx, cy);
// https://math.stackexchange.com/questions/76457/check-if-a-point-is-within-an-ellipse
const pointWithinEllipse = (_x, _y) => (
((_x - cx) ** 2) / rx ** 2) + (((_y - cy) ** 2) / ry ** 2
) <= 1;
if (!pointWithinEllipse(rotX, rotY)) {
// Cursor is outside of an ellipse
return null;
}
if (Math.abs(x - cx) < Number.EPSILON && Math.abs(y - cy) < Number.EPSILON) {
// cursor is near to the center, just return minimum of height, width
return Math.min(rx, ry);
}
// ellipse equation is x^2/rx^2 + y^2/ry^2 = 1
// from this equation:
// x^2 = ((rx * ry)^2 - (y * rx)^2) / ry^2
// y^2 = ((rx * ry)^2 - (x * ry)^2) / rx^2
// we have one point inside the ellipse, let's build two lines (horizontal and vertical) through the point
// and find their interception with ellipse
const x2Equation = (_y) => (((rx * ry) ** 2) - ((_y * rx) ** 2)) / (ry ** 2);
const y2Equation = (_x) => (((rx * ry) ** 2) - ((_x * ry) ** 2)) / (rx ** 2);
// shift x,y to the ellipse coordinate system to compute equation correctly
// y axis is inverted
const [shiftedX, shiftedY] = [x - cx, cy - y];
const [x1, x2] = [Math.sqrt(x2Equation(shiftedY)), -Math.sqrt(x2Equation(shiftedY))];
const [y1, y2] = [Math.sqrt(y2Equation(shiftedX)), -Math.sqrt(y2Equation(shiftedX))];
// found two points on ellipse edge
const ellipseP1X = shiftedX >= 0 ? x1 : x2; // ellipseP1Y is shiftedY
const ellipseP2Y = shiftedY >= 0 ? y1 : y2; // ellipseP1X is shiftedX
// found diffs between two points on edges and target point
const diff1X = ellipseP1X - shiftedX;
const diff2Y = ellipseP2Y - shiftedY;
// return minimum, get absolute value because we need distance, not diff
return Math.min(Math.abs(diff1X), Math.abs(diff2Y));
}
}
class PolyShape extends Shape {}
class PolyShape extends Shape {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.rotation = 0; // is not supported
}
}
class PolygonShape extends PolyShape {
constructor(data, clientID, color, injection) {
@ -1509,6 +1614,7 @@
class CuboidShape extends Shape {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.rotation = 0;
this.shapeType = ObjectShape.CUBOID;
this.pinned = false;
checkNumberOfPoints(this.shapeType, this.points);
@ -1637,11 +1743,40 @@
}
}
interpolatePosition(leftPosition, rightPosition, offset) {
const positionOffset = leftPosition.points.map((point, index) => rightPosition.points[index] - point);
return {
points: leftPosition.points.map((point, index) => point + positionOffset[index] * offset),
rotation:
(leftPosition.rotation + findAngleDiff(
rightPosition.rotation, leftPosition.rotation,
) * offset + 360) % 360,
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
};
}
}
class EllipseTrack extends Track {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shapeType = ObjectShape.ELLIPSE;
this.pinned = false;
for (const shape of Object.values(this.shapes)) {
checkNumberOfPoints(this.shapeType, shape.points);
}
}
interpolatePosition(leftPosition, rightPosition, offset) {
const positionOffset = leftPosition.points.map((point, index) => rightPosition.points[index] - point);
return {
points: leftPosition.points.map((point, index) => point + positionOffset[index] * offset),
rotation:
(leftPosition.rotation + findAngleDiff(
rightPosition.rotation, leftPosition.rotation,
) * offset + 360) % 360,
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
@ -1650,10 +1785,18 @@
}
class PolyTrack extends Track {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
for (const shape of Object.values(this.shapes)) {
shape.rotation = 0; // is not supported
}
}
interpolatePosition(leftPosition, rightPosition, offset) {
if (offset === 0) {
return {
points: [...leftPosition.points],
rotation: leftPosition.rotation,
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
@ -1900,6 +2043,7 @@
return {
points: toArray(reducedPoints),
rotation: leftPosition.rotation,
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
@ -1962,6 +2106,7 @@
points: leftPosition.points.map(
(value, index) => value + (rightPosition.points[index] - value) * offset,
),
rotation: leftPosition.rotation,
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
@ -1970,6 +2115,7 @@
return {
points: [...leftPosition.points],
rotation: leftPosition.rotation,
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
@ -1984,6 +2130,7 @@
this.pinned = false;
for (const shape of Object.values(this.shapes)) {
checkNumberOfPoints(this.shapeType, shape.points);
shape.rotation = 0; // is not supported
}
}
@ -1992,6 +2139,7 @@
return {
points: leftPosition.points.map((point, index) => point + positionOffset[index] * offset),
rotation: leftPosition.rotation,
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
@ -2003,6 +2151,7 @@
PolygonTrack.distance = PolygonShape.distance;
PolylineTrack.distance = PolylineShape.distance;
PointsTrack.distance = PointsShape.distance;
EllipseTrack.distance = EllipseShape.distance;
CuboidTrack.distance = CuboidShape.distance;
module.exports = {
@ -2010,11 +2159,13 @@
PolygonShape,
PolylineShape,
PointsShape,
EllipseShape,
CuboidShape,
RectangleTrack,
PolygonTrack,
PolylineTrack,
PointsTrack,
EllipseTrack,
CuboidTrack,
Track,
Shape,

@ -100,6 +100,7 @@
'occluded',
'z_order',
'points',
'rotation',
'type',
'shapes',
'attributes',

@ -276,7 +276,7 @@
if (instance instanceof Task) {
result = await serverProxy.tasks.exportDataset(instance.id, format, name, saveImages);
} else if (instance instanceof Job) {
result = await serverProxy.tasks.exportDataset(instance.task.id, format, name, saveImages);
result = await serverProxy.tasks.exportDataset(instance.taskId, format, name, saveImages);
} else {
result = await serverProxy.projects.exportDataset(instance.id, format, name, saveImages);
}
@ -284,6 +284,22 @@
return result;
}
function importDataset(instance, format, file, updateStatusCallback = () => {}) {
if (!(typeof format === 'string')) {
throw new ArgumentError('Format must be a string');
}
if (!(instance instanceof Project)) {
throw new ArgumentError('Instance should be a Project instance');
}
if (!(typeof updateStatusCallback === 'function')) {
throw new ArgumentError('Callback should be a function');
}
if (!(['application/zip', 'application/x-zip-compressed'].includes(file.type))) {
throw new ArgumentError('File should be file instance with ZIP extension');
}
return serverProxy.projects.importDataset(instance.id, format, file, updateStatusCallback);
}
function undoActions(session, count) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
@ -366,6 +382,7 @@
importAnnotations,
exportAnnotations,
exportDataset,
importDataset,
undoActions,
redoActions,
freezeHistory,

@ -1,7 +1,9 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
const config = require('./config');
(() => {
const PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy');
@ -9,27 +11,20 @@
const {
isBoolean,
isInteger,
isEnum,
isString,
checkFilter,
checkExclusiveFields,
camelToSnake,
checkObjectType,
} = require('./common');
const {
TaskStatus,
TaskMode,
DimensionType,
CloudStorageProviderType,
CloudStorageCredentialsType,
} = require('./enums');
const User = require('./user');
const { AnnotationFormats } = require('./annotation-formats');
const { ArgumentError } = require('./exceptions');
const { Task } = require('./session');
const { Task, Job } = require('./session');
const { Project } = require('./project');
const { CloudStorage } = require('./cloud-storage');
const Organization = require('./organization');
function implementAPI(cvat) {
cvat.plugins.list.implementation = PluginRegistry.list;
@ -139,7 +134,7 @@
searchParams[key] = filter[key];
}
}
users = await serverProxy.users.get(new URLSearchParams(searchParams).toString());
users = await serverProxy.users.get(searchParams);
}
users = users.map((user) => new User(user));
@ -148,72 +143,67 @@
cvat.jobs.get.implementation = async (filter) => {
checkFilter(filter, {
page: isInteger,
filter: isString,
sort: isString,
search: isString,
taskID: isInteger,
jobID: isInteger,
});
if ('taskID' in filter && 'jobID' in filter) {
throw new ArgumentError('Only one of fields "taskID" and "jobID" allowed simultaneously');
}
if (!Object.keys(filter).length) {
throw new ArgumentError('Job filter must not be empty');
throw new ArgumentError('Filter fields "taskID" and "jobID" are not permitted to be used at the same time');
}
let tasks = [];
if ('taskID' in filter) {
tasks = await serverProxy.tasks.getTasks(`id=${filter.taskID}`);
} else {
const job = await serverProxy.jobs.get(filter.jobID);
if (typeof job.task_id !== 'undefined') {
tasks = await serverProxy.tasks.getTasks(`id=${job.task_id}`);
const [task] = await serverProxy.tasks.get({ id: filter.taskID });
if (task) {
return new Task(task).jobs;
}
return [];
}
// If task was found by its id, then create task instance and get Job instance from it
if (tasks.length) {
const task = new Task(tasks[0]);
return filter.jobID ? task.jobs.filter((job) => job.id === filter.jobID) : task.jobs;
if ('jobID' in filter) {
const job = await serverProxy.jobs.get({ id: filter.jobID });
if (job) {
return [new Job(job)];
}
}
return tasks;
const jobsData = await serverProxy.jobs.get(filter);
const jobs = jobsData.results.map((jobData) => new Job(jobData));
jobs.count = jobsData.count;
return jobs;
};
cvat.tasks.get.implementation = async (filter) => {
checkFilter(filter, {
page: isInteger,
projectId: isInteger,
name: isString,
id: isInteger,
owner: isString,
assignee: isString,
search: isString,
status: isEnum.bind(TaskStatus),
mode: isEnum.bind(TaskMode),
dimension: isEnum.bind(DimensionType),
filter: isString,
ordering: isString,
});
checkExclusiveFields(filter, ['id', 'search', 'projectId'], ['page']);
checkExclusiveFields(filter, ['id', 'projectId'], ['page']);
const searchParams = new URLSearchParams();
const searchParams = {};
for (const field of [
'name',
'owner',
'assignee',
'filter',
'search',
'status',
'mode',
'ordering',
'id',
'page',
'projectId',
'dimension',
]) {
if (Object.prototype.hasOwnProperty.call(filter, field)) {
searchParams.set(field, filter[field]);
searchParams[camelToSnake(field)] = filter[field];
}
}
const tasksData = await serverProxy.tasks.getTasks(searchParams.toString());
const tasksData = await serverProxy.tasks.get(searchParams);
const tasks = tasksData.map((task) => new Task(task));
tasks.count = tasksData.count;
@ -225,40 +215,22 @@
checkFilter(filter, {
id: isInteger,
page: isInteger,
name: isString,
assignee: isString,
owner: isString,
search: isString,
status: isEnum.bind(TaskStatus),
withoutTasks: isBoolean,
filter: isString,
});
checkExclusiveFields(filter, ['id', 'search'], ['page', 'withoutTasks']);
if (typeof filter.withoutTasks === 'undefined') {
if (typeof filter.id === 'undefined') {
filter.withoutTasks = true;
} else {
filter.withoutTasks = false;
}
}
checkExclusiveFields(filter, ['id'], ['page']);
const searchParams = new URLSearchParams();
for (const field of ['name', 'assignee', 'owner', 'search', 'status', 'id', 'page', 'withoutTasks']) {
const searchParams = {};
for (const field of ['filter', 'search', 'status', 'id', 'page']) {
if (Object.prototype.hasOwnProperty.call(filter, field)) {
searchParams.set(camelToSnake(field), filter[field]);
searchParams[camelToSnake(field)] = filter[field];
}
}
const projectsData = await serverProxy.projects.get(searchParams.toString());
// prettier-ignore
const projectsData = await serverProxy.projects.get(searchParams);
const projects = projectsData.map((project) => {
if (filter.withoutTasks) {
project.task_ids = project.tasks;
project.tasks = [];
} else {
project.task_ids = project.tasks.map((task) => task.id);
}
project.task_ids = project.tasks;
return project;
}).map((project) => new Project(project));
@ -273,46 +245,46 @@
cvat.cloudStorages.get.implementation = async (filter) => {
checkFilter(filter, {
page: isInteger,
displayName: isString,
resourceName: isString,
description: isString,
filter: isString,
id: isInteger,
owner: isString,
search: isString,
providerType: isEnum.bind(CloudStorageProviderType),
credentialsType: isEnum.bind(CloudStorageCredentialsType),
});
checkExclusiveFields(filter, ['id', 'search'], ['page']);
const searchParams = new URLSearchParams();
for (const field of [
'displayName',
'credentialsType',
'providerType',
'owner',
'filter',
'search',
'id',
'page',
'description',
]) {
if (Object.prototype.hasOwnProperty.call(filter, field)) {
searchParams.set(camelToSnake(field), filter[field]);
}
}
if (Object.prototype.hasOwnProperty.call(filter, 'resourceName')) {
searchParams.set('resource', filter.resourceName);
}
const cloudStoragesData = await serverProxy.cloudStorages.get(searchParams.toString());
const cloudStorages = cloudStoragesData.map((cloudStorage) => new CloudStorage(cloudStorage));
cloudStorages.count = cloudStoragesData.count;
return cloudStorages;
};
cvat.organizations.get.implementation = async () => {
const organizationsData = await serverProxy.organizations.get();
const organizations = organizationsData.map((organizationData) => new Organization(organizationData));
return organizations;
};
cvat.organizations.activate.implementation = (organization) => {
checkObjectType('organization', organization, null, Organization);
config.organizationID = organization.slug;
};
cvat.organizations.deactivate.implementation = async () => {
config.organizationID = null;
};
return cvat;
}

@ -1,4 +1,4 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -15,7 +15,6 @@ function build() {
const Statistics = require('./statistics');
const Comment = require('./comment');
const Issue = require('./issue');
const Review = require('./review');
const { Job, Task } = require('./session');
const { Project } = require('./project');
const implementProject = require('./project-implementation');
@ -23,6 +22,7 @@ function build() {
const MLModel = require('./ml-model');
const { FrameData } = require('./frames');
const { CloudStorage } = require('./cloud-storage');
const Organization = require('./organization');
const enums = require('./enums');
@ -697,6 +697,9 @@ function build() {
* @property {string} proxy Axios proxy settings.
* For more details please read <a href="https://github.com/axios/axios"> here </a>
* @memberof module:API.cvat.config
* @property {string} origin ui URL origin
* @memberof module:API.cvat.config
* @property {number} uploadChunkSize max size of one data request in mb
* @memberof module:API.cvat.config
*/
get backendAPI() {
@ -711,6 +714,18 @@ function build() {
set proxy(value) {
config.proxy = value;
},
get origin() {
return config.origin;
},
set origin(value) {
config.origin = value;
},
get uploadChunkSize() {
return config.uploadChunkSize;
},
set uploadChunkSize(value) {
config.uploadChunkSize = value;
},
},
/**
* Namespace contains some library information e.g. api version
@ -758,7 +773,7 @@ function build() {
/**
* @typedef {Object} CloudStorageFilter
* @property {string} displayName Check if displayName contains this value
* @property {string} resourceName Check if resourceName contains this value
* @property {string} resource Check if resource name contains this value
* @property {module:API.cvat.enums.ProviderType} providerType Check if providerType equal this value
* @property {integer} id Check if id equals this value
* @property {integer} page Get specific page
@ -784,6 +799,50 @@ function build() {
return result;
},
},
/**
* This namespace could be used to get organizations list from the server
* @namespace organizations
* @memberof module:API.cvat
*/
organizations: {
/**
* Method returns a list of organizations
* @method get
* @async
* @memberof module:API.cvat.organizations
* @returns {module:API.cvat.classes.Organization[]}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async get() {
const result = await PluginRegistry.apiWrapper(cvat.organizations.get);
return result;
},
/**
* Method activates organization context
* @method activate
* @async
* @param {module:API.cvat.classes.Organization}
* @memberof module:API.cvat.organizations
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async activate(organization) {
const result = await PluginRegistry.apiWrapper(cvat.organizations.activate, organization);
return result;
},
/**
* Method deactivates organization context
* @method deactivate
* @async
* @memberof module:API.cvat.organizations
* @throws {module:API.cvat.exceptions.PluginError}
*/
async deactivate() {
const result = await PluginRegistry.apiWrapper(cvat.organizations.deactivate);
return result;
},
},
/**
* Namespace is used for access to classes
* @namespace classes
@ -802,9 +861,9 @@ function build() {
MLModel,
Comment,
Issue,
Review,
FrameData,
CloudStorage,
Organization,
},
};
@ -818,6 +877,7 @@ function build() {
cvat.client = Object.freeze(cvat.client);
cvat.enums = Object.freeze(cvat.enums);
cvat.cloudStorages = Object.freeze(cvat.cloudStorages);
cvat.organizations = Object.freeze(cvat.organizations);
const implementAPI = require('./api-implementation');

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -9,12 +9,19 @@
const { ArgumentError } = require('./exceptions');
const { CloudStorageCredentialsType, CloudStorageProviderType } = require('./enums');
function validateNotEmptyString(value) {
if (typeof value !== 'string') {
throw new ArgumentError(`Value must be a string. ${typeof value} was found`);
} else if (!value.trim().length) {
throw new ArgumentError('Value mustn\'t be empty string');
}
}
/**
* Class representing a cloud storage
* @memberof module:API.cvat.classes
*/
class CloudStorage {
// TODO: add storage availability status (avaliable/unavaliable)
constructor(initialData) {
const data = {
id: undefined,
@ -27,6 +34,7 @@
key: undefined,
secret_key: undefined,
session_token: undefined,
key_file: undefined,
specific_attributes: undefined,
owner: undefined,
created_date: undefined,
@ -65,11 +73,7 @@
displayName: {
get: () => data.display_name,
set: (value) => {
if (typeof value !== 'string') {
throw new ArgumentError(`Value must be string. ${typeof value} was found`);
} else if (!value.trim().length) {
throw new ArgumentError('Value must not be empty string');
}
validateNotEmptyString(value);
data.display_name = value;
},
},
@ -101,15 +105,8 @@
accountName: {
get: () => data.account_name,
set: (value) => {
if (typeof value === 'string') {
if (value.trim().length) {
data.account_name = value;
} else {
throw new ArgumentError('Value must not be empty');
}
} else {
throw new ArgumentError(`Value must be a string. ${typeof value} was found`);
}
validateNotEmptyString(value);
data.account_name = value;
},
},
/**
@ -123,15 +120,8 @@
accessKey: {
get: () => data.key,
set: (value) => {
if (typeof value === 'string') {
if (value.trim().length) {
data.key = value;
} else {
throw new ArgumentError('Value must not be empty');
}
} else {
throw new ArgumentError(`Value must be a string. ${typeof value} was found`);
}
validateNotEmptyString(value);
data.key = value;
},
},
/**
@ -145,15 +135,8 @@
secretKey: {
get: () => data.secret_key,
set: (value) => {
if (typeof value === 'string') {
if (value.trim().length) {
data.secret_key = value;
} else {
throw new ArgumentError('Value must not be empty');
}
} else {
throw new ArgumentError(`Value must be a string. ${typeof value} was found`);
}
validateNotEmptyString(value);
data.secret_key = value;
},
},
/**
@ -167,33 +150,40 @@
token: {
get: () => data.session_token,
set: (value) => {
if (typeof value === 'string') {
if (value.trim().length) {
data.session_token = value;
} else {
throw new ArgumentError('Value must not be empty');
}
validateNotEmptyString(value);
data.session_token = value;
},
},
/**
* Key file
* @name keyFile
* @type {File}
* @memberof module:API.cvat.classes.CloudStorage
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
keyFile: {
get: () => data.key_file,
set: (file) => {
if (file instanceof File) {
data.key_file = file;
} else {
throw new ArgumentError(`Value must be a string. ${typeof value} was found`);
throw new ArgumentError(`Should be a file. ${typeof file} was found`);
}
},
},
/**
* Unique resource name
* @name resourceName
* @name resource
* @type {string}
* @memberof module:API.cvat.classes.CloudStorage
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
resourceName: {
resource: {
get: () => data.resource,
set: (value) => {
if (typeof value !== 'string') {
throw new ArgumentError(`Value must be string. ${typeof value} was found`);
} else if (!value.trim().length) {
throw new ArgumentError('Value must not be empty');
}
validateNotEmptyString(value);
data.resource = value;
},
},
@ -207,11 +197,8 @@
manifestPath: {
get: () => data.manifest_path,
set: (value) => {
if (typeof value === 'string') {
data.manifest_path = value;
} else {
throw new ArgumentError('Value must be a string');
}
validateNotEmptyString(value);
data.manifest_path = value;
},
},
/**
@ -410,7 +397,7 @@
CloudStorage.prototype.save.implementation = async function () {
function prepareOptionalFields(cloudStorageInstance) {
const data = {};
if (cloudStorageInstance.description) {
if (cloudStorageInstance.description !== undefined) {
data.description = cloudStorageInstance.description;
}
@ -430,14 +417,18 @@
data.session_token = cloudStorageInstance.token;
}
if (cloudStorageInstance.specificAttributes) {
if (cloudStorageInstance.keyFile) {
data.key_file = cloudStorageInstance.keyFile;
}
if (cloudStorageInstance.specificAttributes !== undefined) {
data.specific_attributes = cloudStorageInstance.specificAttributes;
}
return data;
}
// update
if (typeof this.id !== 'undefined') {
// providr_type and recource should not change;
// provider_type and recource should not change;
// send to the server only the values that have changed
const initialData = {};
if (this.displayName) {
@ -465,7 +456,7 @@
display_name: this.displayName,
credentials_type: this.credentialsType,
provider_type: this.providerType,
resource: this.resourceName,
resource: this.resource,
manifests: this.manifests,
};

@ -1,10 +1,9 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
const User = require('./user');
const { ArgumentError } = require('./exceptions');
const { negativeIDGenerator } = require('./common');
/**
* Class representing a single comment
@ -18,8 +17,7 @@ class Comment {
message: undefined,
created_date: undefined,
updated_date: undefined,
removed: false,
author: undefined,
owner: undefined,
};
for (const property in data) {
@ -28,11 +26,7 @@ class Comment {
}
}
if (data.author && !(data.author instanceof User)) data.author = new User(data.author);
if (typeof id === 'undefined') {
data.id = negativeIDGenerator();
}
if (data.owner && !(data.owner instanceof User)) data.owner = new User(data.owner);
if (typeof data.created_date === 'undefined') {
data.created_date = new Date().toISOString();
}
@ -88,29 +82,14 @@ class Comment {
},
/**
* Instance of a user who has created the comment
* @name author
* @name owner
* @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;
},
owner: {
get: () => data.owner,
},
__internal: {
get: () => data,
@ -124,7 +103,7 @@ class Comment {
message: this.message,
};
if (this.id > 0) {
if (typeof this.id === 'number') {
data.id = this.id;
}
if (this.createdDate) {
@ -133,21 +112,12 @@ class Comment {
if (this.updatedDate) {
data.updated_date = this.updatedDate;
}
if (this.author) {
data.author = this.author.serialize();
if (this.owner) {
data.owner_id = this.owner.serialize().id;
}
return data;
}
toJSON() {
const data = this.serialize();
const { author, ...updated } = data;
return {
...updated,
author_id: author ? author.id : undefined,
};
}
}
module.exports = Comment;

@ -1,4 +1,4 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -36,7 +36,7 @@
if (!(prop in fields)) {
throw new ArgumentError(`Unsupported filter property has been received: "${prop}"`);
} else if (!fields[prop](filter[prop])) {
throw new ArgumentError(`Received filter property "${prop}" is not satisfied for checker`);
throw new ArgumentError(`Received filter property "${prop}" does not satisfy API`);
}
}
}
@ -97,37 +97,30 @@
);
}
function negativeIDGenerator() {
const value = negativeIDGenerator.start;
negativeIDGenerator.start -= 1;
return value;
}
negativeIDGenerator.start = -1;
class FieldUpdateTrigger {
constructor(initialFields) {
const data = { ...initialFields };
constructor() {
let updatedFlags = {};
Object.defineProperties(
this,
Object.freeze({
...Object.assign(
{},
...Array.from(Object.keys(data), (key) => ({
[key]: {
get: () => data[key],
set: (value) => {
data[key] = value;
},
enumerable: true,
},
})),
),
reset: {
value: () => {
Object.keys(data).forEach((key) => {
data[key] = false;
});
updatedFlags = {};
},
},
update: {
value: (name) => {
updatedFlags[name] = true;
},
},
getUpdated: {
value: (data, propMap = {}) => {
const result = {};
for (const updatedField of Object.keys(updatedFlags)) {
result[propMap[updatedField] || updatedField] = data[updatedField];
}
return result;
},
},
}),
@ -142,7 +135,6 @@
isString,
checkFilter,
checkObjectType,
negativeIDGenerator,
checkExclusiveFields,
camelToSnake,
FieldUpdateTrigger,

@ -1,8 +1,11 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
module.exports = {
backendAPI: '/api/v1',
backendAPI: '/api',
proxy: false,
organizationID: null,
origin: '',
uploadChunkSize: 100,
};

@ -1,4 +1,4 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2022 Intel Corporation
//
// SPDX-License-Identifier: MIT

@ -34,33 +34,51 @@
});
/**
* Task dimension
* @enum
* @name DimensionType
* Job stages
* @enum {string}
* @name JobStage
* @memberof module:API.cvat.enums
* @property {string} DIMENSION_2D '2d'
* @property {string} DIMENSION_3D '3d'
* @property {string} ANNOTATION 'annotation'
* @property {string} VALIDATION 'validation'
* @property {string} ACCEPTANCE 'acceptance'
* @readonly
*/
const DimensionType = Object.freeze({
DIMENSION_2D: '2d',
DIMENSION_3D: '3d',
const JobStage = Object.freeze({
ANNOTATION: 'annotation',
VALIDATION: 'validation',
ACCEPTANCE: 'acceptance',
});
/**
* Review statuses
* Job states
* @enum {string}
* @name ReviewStatus
* @name JobState
* @memberof module:API.cvat.enums
* @property {string} ACCEPTED 'accepted'
* @property {string} NEW 'new'
* @property {string} IN_PROGRESS 'in progress'
* @property {string} COMPLETED 'completed'
* @property {string} REJECTED 'rejected'
* @property {string} REVIEW_FURTHER 'review_further'
* @readonly
*/
const ReviewStatus = Object.freeze({
ACCEPTED: 'accepted',
const JobState = Object.freeze({
NEW: 'new',
IN_PROGRESS: 'in progress',
COMPLETED: 'completed',
REJECTED: 'rejected',
REVIEW_FURTHER: 'review_further',
});
/**
* Task dimension
* @enum
* @name DimensionType
* @memberof module:API.cvat.enums
* @property {string} DIMENSION_2D '2d'
* @property {string} DIMENSION_3D '3d'
* @readonly
*/
const DimensionType = Object.freeze({
DIMENSION_2D: '2d',
DIMENSION_3D: '3d',
});
/**
@ -150,6 +168,7 @@
POLYGON: 'polygon',
POLYLINE: 'polyline',
POINTS: 'points',
ELLIPSE: 'ellipse',
CUBOID: 'cuboid',
});
@ -340,11 +359,13 @@
* @memberof module:API.cvat.enums
* @property {string} AWS_S3 'AWS_S3_BUCKET'
* @property {string} AZURE 'AZURE_CONTAINER'
* @property {string} GOOGLE_CLOUD_STORAGE 'GOOGLE_CLOUD_STORAGE'
* @readonly
*/
const CloudStorageProviderType = Object.freeze({
AWS_S3_BUCKET: 'AWS_S3_BUCKET',
AZURE_CONTAINER: 'AZURE_CONTAINER',
GOOGLE_CLOUD_STORAGE: 'GOOGLE_CLOUD_STORAGE',
});
/**
@ -355,18 +376,57 @@
* @property {string} KEY_SECRET_KEY_PAIR 'KEY_SECRET_KEY_PAIR'
* @property {string} ACCOUNT_NAME_TOKEN_PAIR 'ACCOUNT_NAME_TOKEN_PAIR'
* @property {string} ANONYMOUS_ACCESS 'ANONYMOUS_ACCESS'
* @property {string} KEY_FILE_PATH 'KEY_FILE_PATH'
* @readonly
*/
const CloudStorageCredentialsType = Object.freeze({
KEY_SECRET_KEY_PAIR: 'KEY_SECRET_KEY_PAIR',
ACCOUNT_NAME_TOKEN_PAIR: 'ACCOUNT_NAME_TOKEN_PAIR',
ANONYMOUS_ACCESS: 'ANONYMOUS_ACCESS',
KEY_FILE_PATH: 'KEY_FILE_PATH',
});
/**
* Task statuses
* @enum {string}
* @name MembershipRole
* @memberof module:API.cvat.enums
* @property {string} WORKER 'worker'
* @property {string} SUPERVISOR 'supervisor'
* @property {string} MAINTAINER 'maintainer'
* @property {string} OWNER 'owner'
* @readonly
*/
const MembershipRole = Object.freeze({
WORKER: 'worker',
SUPERVISOR: 'supervisor',
MAINTAINER: 'maintainer',
OWNER: 'owner',
});
/**
* Sorting methods
* @enum {string}
* @name SortingMethod
* @memberof module:API.cvat.enums
* @property {string} LEXICOGRAPHICAL 'lexicographical'
* @property {string} NATURAL 'natural'
* @property {string} PREDEFINED 'predefined'
* @property {string} RANDOM 'random'
* @readonly
*/
const SortingMethod = Object.freeze({
LEXICOGRAPHICAL: 'lexicographical',
NATURAL: 'natural',
PREDEFINED: 'predefined',
RANDOM: 'random',
});
module.exports = {
ShareFileType,
TaskStatus,
ReviewStatus,
JobStage,
JobState,
TaskMode,
AttributeType,
ObjectType,
@ -380,5 +440,7 @@
DimensionType,
CloudStorageProviderType,
CloudStorageCredentialsType,
MembershipRole,
SortingMethod,
};
})();

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -23,6 +23,7 @@
height,
name,
taskID,
jobID,
frameNumber,
startFrame,
stopFrame,
@ -69,6 +70,17 @@
value: taskID,
writable: false,
},
/**
* @name jid
* @type {integer}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
*/
jid: {
value: jobID,
writable: false,
},
/**
* @name number
* @type {integer}
@ -191,7 +203,7 @@
const taskDataCache = frameDataCache[this.tid];
const activeChunk = taskDataCache.activeChunkRequest;
activeChunk.request = serverProxy.frames
.getData(this.tid, activeChunk.chunkNumber)
.getData(this.tid, this.jid, activeChunk.chunkNumber)
.then((chunk) => {
frameDataCache[this.tid].activeChunkRequest.completed = true;
if (!taskDataCache.nextChunkRequest) {
@ -366,7 +378,7 @@
}
class FrameBuffer {
constructor(size, chunkSize, stopFrame, taskID) {
constructor(size, chunkSize, stopFrame, taskID, jobID) {
this._size = size;
this._buffer = {};
this._contextImage = {};
@ -375,6 +387,7 @@
this._stopFrame = stopFrame;
this._activeFillBufferRequest = false;
this._taskID = taskID;
this._jobID = jobID;
}
isContextImageAvailable(frame) {
@ -411,6 +424,7 @@
const frameData = new FrameData({
...frameMeta,
taskID: this._taskID,
jobID: this._jobID,
frameNumber: requestedFrame,
startFrame: frameDataCache[this._taskID].startFrame,
stopFrame: frameDataCache[this._taskID].stopFrame,
@ -463,31 +477,47 @@
let bufferedFrames = new Set();
// if we send one request to get frame 1 with filling the buffer
// then quicky send one more request to get frame 1
// frame 1 will be already decoded and written to buffer
// the second request gets frame 1 from the buffer, removes it from there and returns
// after the first request finishes decoding it tries to get frame 1, but failed
// because frame 1 was already removed from the buffer by the second request
// to prevent this behavior we do not write decoded frames to buffer till the end of decoding all chunks
const buffersToBeCommited = [];
const commitBuffers = () => {
for (const buffer of buffersToBeCommited) {
this._buffer = {
...this._buffer,
...buffer,
};
}
};
// Need to decode chunks in sequence
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => {
for (const chunkIdx in this._requestedChunks) {
if (Object.prototype.hasOwnProperty.call(this._requestedChunks, chunkIdx)) {
try {
const chunkFrames = await this.requestOneChunkFrames(chunkIdx);
if (chunkIdx in this._requestedChunks) {
bufferedFrames = new Set([...bufferedFrames, ...chunkFrames]);
this._buffer = {
...this._buffer,
...this._requestedChunks[chunkIdx].buffer,
};
delete this._requestedChunks[chunkIdx];
if (Object.keys(this._requestedChunks).length === 0) {
resolve(bufferedFrames);
}
} else {
reject(chunkIdx);
break;
for (const chunkIdx of Object.keys(this._requestedChunks)) {
try {
const chunkFrames = await this.requestOneChunkFrames(chunkIdx);
if (chunkIdx in this._requestedChunks) {
bufferedFrames = new Set([...bufferedFrames, ...chunkFrames]);
buffersToBeCommited.push(this._requestedChunks[chunkIdx].buffer);
delete this._requestedChunks[chunkIdx];
if (Object.keys(this._requestedChunks).length === 0) {
commitBuffers();
resolve(bufferedFrames);
}
} catch (error) {
reject(error);
} else {
commitBuffers();
reject(chunkIdx);
break;
}
} catch (error) {
commitBuffers();
reject(error);
break;
}
}
});
@ -508,9 +538,9 @@
}
}
async require(frameNumber, taskID, fillBuffer, frameStep) {
async require(frameNumber, taskID, jobID, fillBuffer, frameStep) {
for (const frame in this._buffer) {
if (frame < frameNumber || frame >= frameNumber + this._size * frameStep) {
if (+frame < frameNumber || +frame >= frameNumber + this._size * frameStep) {
delete this._buffer[frame];
}
}
@ -520,6 +550,7 @@
let frame = new FrameData({
...frameMeta,
taskID,
jobID,
frameNumber,
startFrame: frameDataCache[taskID].startFrame,
stopFrame: frameDataCache[taskID].stopFrame,
@ -548,7 +579,6 @@
} else if (fillBuffer) {
this.clear();
await this.makeFillRequest(frameNumber, frameStep, fillBuffer ? null : 1);
frame = this._buffer[frameNumber];
} else {
this.clear();
@ -576,10 +606,10 @@
}
}
async function getImageContext(taskID, frame) {
async function getImageContext(jobID, frame) {
return new Promise((resolve, reject) => {
serverProxy.frames
.getImageContext(taskID, frame)
.getImageContext(jobID, frame)
.then((result) => {
if (isNode) {
// eslint-disable-next-line no-undef
@ -598,20 +628,20 @@
});
}
async function getContextImage(taskID, frame) {
async function getContextImage(taskID, jobID, frame) {
if (frameDataCache[taskID].frameBuffer.isContextImageAvailable(frame)) {
return frameDataCache[taskID].frameBuffer.getContextImage(frame);
}
const response = getImageContext(taskID, frame);
const response = getImageContext(jobID, frame);
frameDataCache[taskID].frameBuffer.addContextImage(frame, response);
return frameDataCache[taskID].frameBuffer.getContextImage(frame);
}
async function getPreview(taskID) {
async function getPreview(taskID = null, jobID = null) {
return new Promise((resolve, reject) => {
// Just go to server and get preview (no any cache)
serverProxy.frames
.getPreview(taskID)
.getPreview(taskID, jobID)
.then((result) => {
if (isNode) {
// eslint-disable-next-line no-undef
@ -632,6 +662,7 @@
async function getFrame(
taskID,
jobID,
chunkSize,
chunkType,
mode,
@ -674,6 +705,7 @@
chunkSize,
stopFrame,
taskID,
jobID,
),
decodedBlocksCacheSize,
activeChunkRequest: null,
@ -684,7 +716,7 @@
frameDataCache[taskID].provider.setRenderSize(frameMeta.width, frameMeta.height);
}
return frameDataCache[taskID].frameBuffer.require(frame, taskID, isPlaying, step);
return frameDataCache[taskID].frameBuffer.require(frame, taskID, jobID, isPlaying, step);
}
function getRanges(taskID) {

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -8,7 +8,6 @@ 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');
/**
@ -20,14 +19,13 @@ class Issue {
constructor(initialData) {
const data = {
id: undefined,
job: undefined,
position: undefined,
comment_set: [],
comments: [],
frame: undefined,
created_date: undefined,
resolved_date: undefined,
owner: undefined,
resolver: undefined,
removed: false,
resolved: undefined,
};
for (const property in data) {
@ -37,15 +35,11 @@ class Issue {
}
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 (data.comments) {
data.comments = data.comments.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();
}
@ -81,6 +75,18 @@ class Issue {
data.position = value;
},
},
/**
* ID of a job, the issue is linked with
* @name job
* @type {number}
* @memberof module:API.cvat.classes.Issue
* @instance
* @readonly
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
job: {
get: () => data.job,
},
/**
* List of comments attached to the issue
* @name comments
@ -91,7 +97,7 @@ class Issue {
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
comments: {
get: () => data.comment_set.filter((comment) => !comment.removed),
get: () => [...data.comments],
},
/**
* @name frame
@ -113,16 +119,6 @@ class Issue {
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
@ -135,30 +131,15 @@ class Issue {
get: () => data.owner,
},
/**
* An instance of a user who has resolved the issue
* @name resolver
* The flag defines issue status
* @name resolved
* @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;
},
resolved: {
get: () => data.resolved,
},
__internal: {
get: () => data,
@ -184,7 +165,6 @@ class Issue {
/**
* @typedef {Object} CommentData
* @property {number} [author] an ID of a user who has created the comment
* @property {string} message a comment message
* @global
*/
@ -241,94 +221,95 @@ class Issue {
return result;
}
/**
* The method deletes the issue
* Deletes local or server-saved issues
* @method delete
* @memberof module:API.cvat.classes.Issue
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async delete() {
await PluginRegistry.apiWrapper.call(this, Issue.prototype.delete);
}
serialize() {
const { comments } = this;
const data = {
position: this.position,
frame: this.frame,
comment_set: comments.map((comment) => comment.serialize()),
comments: comments.map((comment) => comment.serialize()),
};
if (this.id > 0) {
if (typeof this.id === 'number') {
data.id = this.id;
}
if (this.createdDate) {
data.created_date = this.createdDate;
if (typeof this.job === 'number') {
data.job = this.job;
}
if (this.resolvedDate) {
data.resolved_date = this.resolvedDate;
if (typeof this.createdDate === 'string') {
data.created_date = this.createdDate;
}
if (this.owner) {
data.owner = this.owner.toJSON();
if (typeof this.resolved === 'boolean') {
data.resolved = this.resolved;
}
if (this.resolver) {
data.resolver = this.resolver.toJSON();
if (this.owner instanceof User) {
data.owner = this.owner.serialize().id;
}
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}`);
throw new ArgumentError(`The argument "data" must be an 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}`);
throw new ArgumentError(`Comment message must be a not empty string. Got "${data.message}"`);
}
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);
if (typeof this.id === 'number') {
const serialized = comment.serialize();
serialized.issue = this.id;
const response = await serverProxy.comments.create(serialized);
const savedComment = new Comment(response);
this.__internal.comment_set.push(savedComment);
this.__internal.comments.push(savedComment);
} else {
this.__internal.comment_set.push(comment);
this.__internal.comments.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}`);
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);
if (typeof this.id === 'number') {
const response = await serverProxy.issues.update(this.id, { resolved: true });
this.__internal.resolved = response.resolved;
} else {
this.__internal.resolved_date = new Date().toISOString();
this.__internal.resolver = user;
this.__internal.resolved = true;
}
};
Issue.prototype.reopen.implementation = async function () {
if (typeof this.id === 'number') {
const response = await serverProxy.issues.update(this.id, { resolved: false });
this.__internal.resolved = response.resolved;
} else {
this.__internal.resolved = false;
}
};
Issue.prototype.delete.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;
await serverProxy.issues.delete(id);
}
};

@ -1,10 +1,9 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
const serverProxy = require('./server-proxy');
const { ArgumentError } = require('./exceptions');
const { Task } = require('./session');
const MLModel = require('./ml-model');
const { RQStatus } = require('./enums');
@ -35,11 +34,9 @@ class LambdaManager {
return models;
}
async run(task, model, args) {
if (!(task instanceof Task)) {
throw new ArgumentError(
`Argument task is expected to be an instance of Task class, but got ${typeof task}`,
);
async run(taskID, model, args) {
if (!Number.isInteger(taskID) || taskID < 0) {
throw new ArgumentError(`Argument taskID must be a positive integer. Got "${taskID}"`);
}
if (!(model instanceof MLModel)) {
@ -52,17 +49,26 @@ class LambdaManager {
throw new ArgumentError(`Argument args is expected to be an object, but got ${typeof model}`);
}
const body = args;
body.task = task.id;
body.function = model.id;
const body = {
...args,
task: taskID,
function: model.id,
};
const result = await serverProxy.lambda.run(body);
return result.id;
}
async call(task, model, args) {
const body = args;
body.task = task.id;
async call(taskID, model, args) {
if (!Number.isInteger(taskID) || taskID < 0) {
throw new ArgumentError(`Argument taskID must be a positive integer. Got "${taskID}"`);
}
const body = {
...args,
task: taskID,
};
const result = await serverProxy.lambda.call(model.id, body);
return result;
}

@ -28,6 +28,7 @@ const { Source } = require('./enums');
descriptions: [],
points: null,
rotation: null,
outside: null,
occluded: null,
keyframe: null,
@ -204,6 +205,28 @@ const { Source } = require('./enums');
}
},
},
rotation: {
/**
* @name rotation
* @type {number} angle measured by degrees
* @memberof module:API.cvat.classes.ObjectState
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
*/
get: () => data.rotation,
set: (rotation) => {
if (typeof rotation === 'number') {
data.updateFlags.points = true;
data.rotation = rotation;
} else {
throw new ArgumentError(
`Rotation is expected to be a number, but got ${
typeof rotation === 'object' ? rotation.constructor.name : typeof points
}`,
);
}
},
},
group: {
/**
* Object with short group info { color, id }
@ -410,6 +433,9 @@ const { Source } = require('./enums');
if (typeof serialized.color === 'string') {
this.color = serialized.color;
}
if (typeof serialized.rotation === 'number') {
this.rotation = serialized.rotation;
}
if (Array.isArray(serialized.points)) {
this.points = serialized.points;
}

@ -0,0 +1,378 @@
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
const { checkObjectType, isEnum } = require('./common');
const config = require('./config');
const { MembershipRole } = require('./enums');
const { ArgumentError, ServerError } = require('./exceptions');
const PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy');
const User = require('./user');
/**
* Class representing an organization
* @memberof module:API.cvat.classes
*/
class Organization {
/**
* @param {object} initialData - Object which is used for initialization
* <br> It must contains keys:
* <br> <li style="margin-left: 10px;"> slug
* <br> It can contains keys:
* <br> <li style="margin-left: 10px;"> name
* <br> <li style="margin-left: 10px;"> description
* <br> <li style="margin-left: 10px;"> owner
* <br> <li style="margin-left: 10px;"> created_date
* <br> <li style="margin-left: 10px;"> updated_date
* <br> <li style="margin-left: 10px;"> contact
*/
constructor(initialData) {
const data = {
id: undefined,
slug: undefined,
name: undefined,
description: undefined,
created_date: undefined,
updated_date: undefined,
owner: undefined,
contact: undefined,
};
for (const prop of Object.keys(data)) {
if (prop in initialData) {
data[prop] = initialData[prop];
}
}
if (data.owner) data.owner = new User(data.owner);
checkObjectType('slug', data.slug, 'string');
if (typeof data.name !== 'undefined') {
checkObjectType('name', data.name, 'string');
}
if (typeof data.description !== 'undefined') {
checkObjectType('description', data.description, 'string');
}
if (typeof data.id !== 'undefined') {
checkObjectType('id', data.id, 'number');
}
if (typeof data.contact !== 'undefined') {
checkObjectType('contact', data.contact, 'object');
for (const prop in data.contact) {
if (typeof data.contact[prop] !== 'string') {
throw ArgumentError(`Contact fields must be strings, tried to set ${typeof data.contact[prop]}`);
}
}
}
if (typeof data.owner !== 'undefined' && data.owner !== null) {
checkObjectType('owner', data.owner, null, User);
}
Object.defineProperties(this, {
id: {
get: () => data.id,
},
slug: {
get: () => data.slug,
},
name: {
get: () => data.name,
set: (name) => {
if (typeof name !== 'string') {
throw ArgumentError(`Name property must be a string, tried to set ${typeof description}`);
}
data.name = name;
},
},
description: {
get: () => data.description,
set: (description) => {
if (typeof description !== 'string') {
throw ArgumentError(
`Description property must be a string, tried to set ${typeof description}`,
);
}
data.description = description;
},
},
contact: {
get: () => ({ ...data.contact }),
set: (contact) => {
if (typeof contact !== 'object') {
throw ArgumentError(`Contact property must be an object, tried to set ${typeof contact}`);
}
for (const prop in contact) {
if (typeof contact[prop] !== 'string') {
throw ArgumentError(`Contact fields must be strings, tried to set ${typeof contact[prop]}`);
}
}
data.contact = { ...contact };
},
},
owner: {
get: () => data.owner,
},
createdDate: {
get: () => data.created_date,
},
updatedDate: {
get: () => data.updated_date,
},
});
}
/**
* Method updates organization data if it was created before, or creates a new organization
* @method save
* @returns {module:API.cvat.classes.Organization}
* @memberof module:API.cvat.classes.Organization
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async save() {
const result = await PluginRegistry.apiWrapper.call(this, Organization.prototype.save);
return result;
}
/**
* Method returns paginatable list of organization members
* @method save
* @returns {module:API.cvat.classes.Organization}
* @param page page number
* @param page_size number of results per page
* @memberof module:API.cvat.classes.Organization
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
async members(page = 1, page_size = 10) {
const result = await PluginRegistry.apiWrapper.call(
this,
Organization.prototype.members,
this.slug,
page,
page_size,
);
return result;
}
/**
* Method removes the organization
* @method remove
* @returns {module:API.cvat.classes.Organization}
* @memberof module:API.cvat.classes.Organization
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async remove() {
const result = await PluginRegistry.apiWrapper.call(this, Organization.prototype.remove);
return result;
}
/**
* Method invites new members by email
* @method invite
* @returns {module:API.cvat.classes.Organization}
* @param {string} email
* @param {string} role
* @memberof module:API.cvat.classes.Organization
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async invite(email, role) {
const result = await PluginRegistry.apiWrapper.call(this, Organization.prototype.invite, email, role);
return result;
}
/**
* Method allows a user to get out from an organization
* The difference between deleteMembership is that membershipId is unknown in this case
* @method leave
* @returns {module:API.cvat.classes.Organization}
* @memberof module:API.cvat.classes.Organization
* @param {module:API.cvat.classes.User} user
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async leave(user) {
const result = await PluginRegistry.apiWrapper.call(this, Organization.prototype.leave, user);
return result;
}
/**
* Method allows to change a membership role
* @method updateMembership
* @returns {module:API.cvat.classes.Organization}
* @param {number} membershipId
* @param {string} role
* @memberof module:API.cvat.classes.Organization
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async updateMembership(membershipId, role) {
const result = await PluginRegistry.apiWrapper.call(
this,
Organization.prototype.updateMembership,
membershipId,
role,
);
return result;
}
/**
* Method allows to kick a user from an organization
* @method deleteMembership
* @returns {module:API.cvat.classes.Organization}
* @param {number} membershipId
* @memberof module:API.cvat.classes.Organization
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async deleteMembership(membershipId) {
const result = await PluginRegistry.apiWrapper.call(
this,
Organization.prototype.deleteMembership,
membershipId,
);
return result;
}
}
Organization.prototype.save.implementation = async function () {
if (typeof this.id === 'number') {
const organizationData = {
name: this.name || this.slug,
description: this.description,
contact: this.contact,
};
const result = await serverProxy.organizations.update(this.id, organizationData);
return new Organization(result);
}
const organizationData = {
slug: this.slug,
name: this.name || this.slug,
description: this.description,
contact: this.contact,
};
const result = await serverProxy.organizations.create(organizationData);
return new Organization(result);
};
Organization.prototype.members.implementation = async function (orgSlug, page, pageSize) {
checkObjectType('orgSlug', orgSlug, 'string');
checkObjectType('page', page, 'number');
checkObjectType('pageSize', pageSize, 'number');
const result = await serverProxy.organizations.members(orgSlug, page, pageSize);
await Promise.all(
result.results.map((membership) => {
const { invitation } = membership;
membership.user = new User(membership.user);
if (invitation) {
return serverProxy.organizations
.invitation(invitation)
.then((invitationData) => {
membership.invitation = invitationData;
})
.catch(() => {
membership.invitation = null;
});
}
return Promise.resolve();
}),
);
result.results.count = result.count;
return result.results;
};
Organization.prototype.remove.implementation = async function () {
if (typeof this.id === 'number') {
await serverProxy.organizations.delete(this.id);
config.organizationID = null;
}
};
Organization.prototype.invite.implementation = async function (email, role) {
checkObjectType('email', email, 'string');
if (!isEnum.bind(MembershipRole)(role)) {
throw new ArgumentError(`Role must be one of: ${Object.values(MembershipRole).toString()}`);
}
if (typeof this.id === 'number') {
await serverProxy.organizations.invite(this.id, { email, role });
}
};
Organization.prototype.updateMembership.implementation = async function (membershipId, role) {
checkObjectType('membershipId', membershipId, 'number');
if (!isEnum.bind(MembershipRole)(role)) {
throw new ArgumentError(`Role must be one of: ${Object.values(MembershipRole).toString()}`);
}
if (typeof this.id === 'number') {
await serverProxy.organizations.updateMembership(membershipId, { role });
}
};
Organization.prototype.deleteMembership.implementation = async function (membershipId) {
checkObjectType('membershipId', membershipId, 'number');
if (typeof this.id === 'number') {
await serverProxy.organizations.deleteMembership(membershipId);
}
};
Organization.prototype.leave.implementation = async function (user) {
checkObjectType('user', user, null, User);
if (typeof this.id === 'number') {
const result = await serverProxy.organizations.members(this.slug, 1, 10, {
filter: JSON.stringify({
and: [{
'==': [{ var: 'user' }, user.id],
}],
}),
});
const [membership] = result.results;
if (!membership) {
throw new ServerError(`Could not find membership for user ${user.username} in organization ${this.slug}`);
}
await serverProxy.organizations.deleteMembership(membership.id);
}
};
module.exports = Organization;

@ -7,40 +7,40 @@
const { getPreview } = require('./frames');
const { Project } = require('./project');
const { exportDataset } = require('./annotations');
const { exportDataset, importDataset } = require('./annotations');
function implementProject(projectClass) {
projectClass.prototype.save.implementation = async function () {
const trainingProjectCopy = this.trainingProject;
if (typeof this.id !== 'undefined') {
// project has been already created, need to update some data
const projectData = {
name: this.name,
assignee_id: this.assignee ? this.assignee.id : null,
bug_tracker: this.bugTracker,
labels: [...this._internalData.labels.map((el) => el.toJSON())],
};
if (trainingProjectCopy) {
projectData.training_project = trainingProjectCopy;
const projectData = this._updateTrigger.getUpdated(this, {
bugTracker: 'bug_tracker',
trainingProject: 'training_project',
assignee: 'assignee_id',
});
if (projectData.assignee_id) {
projectData.assignee_id = projectData.assignee_id.id;
}
if (projectData.labels) {
projectData.labels = projectData.labels.map((el) => el.toJSON());
}
await serverProxy.projects.save(this.id, projectData);
this._updateTrigger.reset();
return this;
}
// initial creating
const projectSpec = {
name: this.name,
labels: [...this.labels.map((el) => el.toJSON())],
labels: this.labels.map((el) => el.toJSON()),
};
if (this.bugTracker) {
projectSpec.bug_tracker = this.bugTracker;
}
if (trainingProjectCopy) {
projectSpec.training_project = trainingProjectCopy;
if (this.trainingProject) {
projectSpec.training_project = this.trainingProject;
}
const project = await serverProxy.projects.create(projectSpec);
@ -61,11 +61,30 @@
};
projectClass.prototype.annotations.exportDataset.implementation = async function (
format, saveImages, customName,
format,
saveImages,
customName,
) {
const result = exportDataset(this, format, customName, saveImages);
return result;
};
projectClass.prototype.annotations.importDataset.implementation = async function (
format,
file,
updateStatusCallback,
) {
return importDataset(this, format, file, updateStatusCallback);
};
projectClass.prototype.backup.implementation = async function () {
const result = await serverProxy.projects.backupProject(this.id);
return result;
};
projectClass.restore.implementation = async function (file) {
const result = await serverProxy.projects.restoreProject(file);
return result.id;
};
return projectClass;
}

@ -5,9 +5,9 @@
(() => {
const PluginRegistry = require('./plugins');
const { ArgumentError } = require('./exceptions');
const { Task } = require('./session');
const { Label } = require('./labels');
const User = require('./user');
const { FieldUpdateTrigger } = require('./common');
/**
* Class representing a project
@ -37,6 +37,8 @@
dimension: undefined,
};
const updateTrigger = new FieldUpdateTrigger();
for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
data[property] = initialData[property];
@ -44,7 +46,6 @@
}
data.labels = [];
data.tasks = [];
if (Array.isArray(initialData.labels)) {
for (const label of initialData.labels) {
@ -53,19 +54,6 @@
}
}
if (Array.isArray(initialData.tasks)) {
for (const task of initialData.tasks) {
const taskInstance = new Task(task);
data.tasks.push(taskInstance);
}
}
if (!data.task_subsets) {
const subsetsSet = new Set();
for (const task of data.tasks) {
if (task.subset) subsetsSet.add(task.subset);
}
data.task_subsets = Array.from(subsetsSet);
}
if (typeof initialData.training_project === 'object') {
data.training_project = { ...initialData.training_project };
}
@ -97,6 +85,7 @@
throw new ArgumentError('Value must not be empty');
}
data.name = value;
updateTrigger.update('name');
},
},
@ -125,6 +114,7 @@
throw new ArgumentError('Value must be a user instance');
}
data.assignee = assignee;
updateTrigger.update('assignee');
},
},
/**
@ -149,6 +139,7 @@
get: () => data.bug_tracker,
set: (tracker) => {
data.bug_tracker = tracker;
updateTrigger.update('bugTracker');
},
},
/**
@ -210,19 +201,9 @@
});
data.labels = [...deletedLabels, ...labels];
updateTrigger.update('labels');
},
},
/**
* Tasks related with the project
* @name tasks
* @type {module:API.cvat.classes.Task[]}
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
*/
tasks: {
get: () => [...data.tasks],
},
/**
* Subsets array for related tasks
* @name subsets
@ -257,11 +238,15 @@
} else {
data.training_project = updatedProject;
}
updateTrigger.update('trainingProject');
},
},
_internalData: {
get: () => data,
},
_updateTrigger: {
get: () => updateTrigger,
},
}),
);
@ -270,6 +255,7 @@
// So, we need return it
this.annotations = {
exportDataset: Object.getPrototypeOf(this).annotations.exportDataset.bind(this),
importDataset: Object.getPrototypeOf(this).annotations.importDataset.bind(this),
};
}
@ -319,6 +305,38 @@
const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.delete);
return result;
}
/**
* Method makes a backup of a project
* @method export
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
* @returns {string} URL to get result archive
*/
async backup() {
const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.backup);
return result;
}
/**
* Method restores a project from a backup
* @method restore
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
* @returns {number} ID of the imported project
*/
static async restore(file) {
const result = await PluginRegistry.apiWrapper.call(this, Project.restore, file);
return result;
}
}
Object.defineProperties(
@ -336,6 +354,16 @@
);
return result;
},
async importDataset(format, file, updateStatusCallback = null) {
const result = await PluginRegistry.apiWrapper.call(
this,
Project.prototype.annotations.importDataset,
format,
file,
updateStatusCallback,
);
return result;
},
},
writable: true,
}),

@ -1,397 +0,0 @@
// 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;

File diff suppressed because it is too large Load Diff

@ -1,9 +1,8 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
(() => {
const store = require('store');
const PluginRegistry = require('./plugins');
const loggerStorage = require('./logger-storage');
const serverProxy = require('./server-proxy');
@ -11,12 +10,11 @@
getFrame, getRanges, getPreview, clear: clearFrames, getContextImage,
} = require('./frames');
const { ArgumentError, DataError } = require('./exceptions');
const { TaskStatus } = require('./enums');
const { JobStage, JobState } = require('./enums');
const { Label } = require('./labels');
const User = require('./user');
const Issue = require('./issue');
const Review = require('./review');
const { FieldUpdateTrigger } = require('./common');
const { FieldUpdateTrigger, checkObjectType } = require('./common');
function buildDuplicatedAPI(prototype) {
Object.defineProperties(prototype, {
@ -180,11 +178,10 @@
const result = await PluginRegistry.apiWrapper.call(this, prototype.frames.preview);
return result;
},
async contextImage(taskId, frameId) {
async contextImage(frameId) {
const result = await PluginRegistry.apiWrapper.call(
this,
prototype.frames.contextImage,
taskId,
frameId,
);
return result;
@ -709,18 +706,21 @@
const data = {
id: undefined,
assignee: null,
reviewer: null,
status: undefined,
stage: undefined,
state: undefined,
start_frame: undefined,
stop_frame: undefined,
task: undefined,
project_id: null,
task_id: undefined,
labels: undefined,
dimension: undefined,
data_compressed_chunk_type: undefined,
data_chunk_size: undefined,
bug_tracker: null,
mode: undefined,
};
const updatedFields = new FieldUpdateTrigger({
assignee: false,
reviewer: false,
status: false,
});
const updateTrigger = new FieldUpdateTrigger();
for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property)) {
@ -735,7 +735,19 @@
}
if (data.assignee) data.assignee = new User(data.assignee);
if (data.reviewer) data.reviewer = new User(data.reviewer);
if (Array.isArray(initialData.labels)) {
data.labels = initialData.labels.map((labelData) => {
// can be already wrapped to the class
// when create this job from Task constructor
if (labelData instanceof Label) {
return labelData;
}
return new Label(labelData);
});
} else {
throw new Error('Job labels must be an array');
}
Object.defineProperties(
this,
@ -764,42 +776,53 @@
if (assignee !== null && !(assignee instanceof User)) {
throw new ArgumentError('Value must be a user instance');
}
updatedFields.assignee = true;
updateTrigger.update('assignee');
data.assignee = assignee;
},
},
/**
* Instance of a user who is responsible for review
* @name reviewer
* @type {module:API.cvat.classes.User}
* @name stage
* @type {module:API.cvat.enums.JobStage}
* @memberof module:API.cvat.classes.Job
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
reviewer: {
get: () => data.reviewer,
set: (reviewer) => {
if (reviewer !== null && !(reviewer instanceof User)) {
throw new ArgumentError('Value must be a user instance');
stage: {
get: () => data.stage,
set: (stage) => {
const type = JobStage;
let valueInEnum = false;
for (const value in type) {
if (type[value] === stage) {
valueInEnum = true;
break;
}
}
updatedFields.reviewer = true;
data.reviewer = reviewer;
if (!valueInEnum) {
throw new ArgumentError(
'Value must be a value from the enumeration cvat.enums.JobStage',
);
}
updateTrigger.update('stage');
data.stage = stage;
},
},
/**
* @name status
* @type {module:API.cvat.enums.TaskStatus}
* @name state
* @type {module:API.cvat.enums.JobState}
* @memberof module:API.cvat.classes.Job
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
status: {
get: () => data.status,
set: (status) => {
const type = TaskStatus;
state: {
get: () => data.state,
set: (state) => {
const type = JobState;
let valueInEnum = false;
for (const value in type) {
if (type[value] === status) {
if (type[value] === state) {
valueInEnum = true;
break;
}
@ -807,12 +830,12 @@
if (!valueInEnum) {
throw new ArgumentError(
'Value must be a value from the enumeration cvat.enums.TaskStatus',
'Value must be a value from the enumeration cvat.enums.JobState',
);
}
updatedFields.status = true;
data.status = status;
updateTrigger.update('state');
data.state = state;
},
},
/**
@ -836,17 +859,96 @@
get: () => data.stop_frame,
},
/**
* @name task
* @type {module:API.cvat.classes.Task}
* @name projectId
* @type {integer|null}
* @memberof module:API.cvat.classes.Job
* @readonly
* @instance
*/
task: {
get: () => data.task,
projectId: {
get: () => data.project_id,
},
/**
* @name taskId
* @type {integer}
* @memberof module:API.cvat.classes.Job
* @readonly
* @instance
*/
taskId: {
get: () => data.task_id,
},
/**
* @name labels
* @type {module:API.cvat.classes.Label[]}
* @memberof module:API.cvat.classes.Job
* @readonly
* @instance
*/
labels: {
get: () => data.labels.filter((_label) => !_label.deleted),
},
/**
* @name dimension
* @type {module:API.cvat.enums.DimensionType}
* @memberof module:API.cvat.classes.Task
* @readonly
* @instance
*/
dimension: {
get: () => data.dimension,
},
/**
* @name dataChunkSize
* @type {integer}
* @memberof module:API.cvat.classes.Job
* @readonly
* @instance
*/
dataChunkSize: {
get: () => data.data_chunk_size,
set: (chunkSize) => {
if (typeof chunkSize !== 'number' || chunkSize < 1) {
throw new ArgumentError(
`Chunk size value must be a positive number. But value ${chunkSize} has been got.`,
);
}
data.data_chunk_size = chunkSize;
},
},
/**
* @name dataChunkSize
* @type {string}
* @memberof module:API.cvat.classes.Job
* @readonly
* @instance
*/
dataChunkType: {
get: () => data.data_compressed_chunk_type,
},
/**
* @name mode
* @type {string}
* @memberof module:API.cvat.classes.Job
* @readonly
* @instance
*/
mode: {
get: () => data.mode,
},
/**
* @name bugTracker
* @type {string|null}
* @memberof module:API.cvat.classes.Job
* @instance
* @readonly
*/
bugTracker: {
get: () => data.bug_tracker,
},
__updatedFields: {
get: () => updatedFields,
_updateTrigger: {
get: () => updateTrigger,
},
}),
);
@ -870,6 +972,7 @@
export: Object.getPrototypeOf(this).annotations.export.bind(this),
statistics: Object.getPrototypeOf(this).annotations.statistics.bind(this),
hasUnsavedChanges: Object.getPrototypeOf(this).annotations.hasUnsavedChanges.bind(this),
exportDataset: Object.getPrototypeOf(this).annotations.exportDataset.bind(this),
};
this.actions = {
@ -898,7 +1001,7 @@
}
/**
* Method updates job data like status or assignee
* Method updates job data like state, stage or assignee
* @method save
* @memberof module:API.cvat.classes.Job
* @readonly
@ -916,7 +1019,7 @@
* Method returns a list of issues for a job
* @method issues
* @memberof module:API.cvat.classes.Job
* @type {module:API.cvat.classes.Issue[]}
* @returns {module:API.cvat.classes.Issue[]}
* @readonly
* @instance
* @async
@ -929,44 +1032,36 @@
}
/**
* Method returns a list of reviews for a job
* @method reviews
* @type {module:API.cvat.classes.Review[]}
* Method adds a new issue to a job
* @method openIssue
* @memberof module:API.cvat.classes.Job
* @returns {module:API.cvat.classes.Issue}
* @param {module:API.cvat.classes.Issue} issue
* @param {string} message
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async reviews() {
const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.reviews);
async openIssue(issue, message) {
const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.openIssue, issue, message);
return result;
}
/**
* /**
* @typedef {Object} ReviewSummary
* @property {number} reviews Number of done reviews
* @property {number} average_estimated_quality
* @property {number} issues_unsolved
* @property {number} issues_resolved
* @property {string[]} assignees
* @property {string[]} reviewers
*/
/**
* Method returns brief summary of within all reviews
* @method reviewsSummary
* @type {ReviewSummary}
* Method removes all job related data from the client (annotations, history, etc.)
* @method close
* @returns {module:API.cvat.classes.Job}
* @memberof module:API.cvat.classes.Job
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @instance
* @throws {module:API.cvat.exceptions.PluginError}
*/
async reviewsSummary() {
const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.reviewsSummary);
async close() {
const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.close);
return result;
}
}
@ -993,7 +1088,7 @@
const data = {
id: undefined,
name: undefined,
project_id: undefined,
project_id: null,
status: undefined,
size: undefined,
mode: undefined,
@ -1017,16 +1112,10 @@
copy_data: undefined,
dimension: undefined,
cloud_storage_id: undefined,
sorting_method: undefined,
};
const updatedFields = new FieldUpdateTrigger({
name: false,
assignee: false,
bug_tracker: false,
subset: false,
labels: false,
project_id: false,
});
const updateTrigger = new FieldUpdateTrigger();
for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
@ -1045,6 +1134,13 @@
remote_files: [],
});
if (Array.isArray(initialData.labels)) {
for (const label of initialData.labels) {
const classInstance = new Label(label);
data.labels.push(classInstance);
}
}
if (Array.isArray(initialData.segments)) {
for (const segment of initialData.segments) {
if (Array.isArray(segment.jobs)) {
@ -1053,25 +1149,28 @@
url: job.url,
id: job.id,
assignee: job.assignee,
reviewer: job.reviewer,
status: job.status,
state: job.state,
stage: job.stage,
start_frame: segment.start_frame,
stop_frame: segment.stop_frame,
task: this,
// following fields also returned when doing API request /jobs/<id>
// here we know them from task and append to constructor
task_id: data.id,
project_id: data.project_id,
labels: data.labels,
bug_tracker: data.bug_tracker,
mode: data.mode,
dimension: data.dimension,
data_compressed_chunk_type: data.data_compressed_chunk_type,
data_chunk_size: data.data_chunk_size,
});
data.jobs.push(jobInstance);
}
}
}
}
if (Array.isArray(initialData.labels)) {
for (const label of initialData.labels) {
const classInstance = new Label(label);
data.labels.push(classInstance);
}
}
Object.defineProperties(
this,
Object.freeze({
@ -1098,7 +1197,7 @@
if (!value.trim().length) {
throw new ArgumentError('Value must not be empty');
}
updatedFields.name = true;
updateTrigger.update('name');
data.name = value;
},
},
@ -1115,7 +1214,7 @@
throw new ArgumentError('Value must be a positive integer');
}
updatedFields.project_id = true;
updateTrigger.update('projectId');
data.project_id = projectId;
},
},
@ -1174,7 +1273,7 @@
if (assignee !== null && !(assignee instanceof User)) {
throw new ArgumentError('Value must be a user instance');
}
updatedFields.assignee = true;
updateTrigger.update('assignee');
data.assignee = assignee;
},
},
@ -1214,7 +1313,7 @@
);
}
updatedFields.bug_tracker = true;
updateTrigger.update('bugTracker');
data.bug_tracker = tracker;
},
},
@ -1234,7 +1333,7 @@
);
}
updatedFields.subset = true;
updateTrigger.update('subset');
data.subset = subset;
},
},
@ -1335,7 +1434,6 @@
},
},
/**
* After task has been created value can be appended only.
* @name labels
* @type {module:API.cvat.classes.Label[]}
* @memberof module:API.cvat.classes.Task
@ -1363,7 +1461,7 @@
_label.deleted = true;
});
updatedFields.labels = true;
updateTrigger.update('labels');
data.labels = [...deletedLabels, ...labels];
},
},
@ -1530,14 +1628,14 @@
dataChunkType: {
get: () => data.data_compressed_chunk_type,
},
/**
* @name dimension
* @type {module:API.cvat.enums.DimensionType}
* @memberof module:API.cvat.classes.Task
* @readonly
* @instance
*/
dimension: {
/**
* @name enabled
* @type {string}
* @memberof module:API.cvat.enums.DimensionType
* @readonly
* @instance
*/
get: () => data.dimension,
},
/**
@ -1549,11 +1647,21 @@
cloudStorageId: {
get: () => data.cloud_storage_id,
},
sortingMethod: {
/**
* @name sortingMethod
* @type {module:API.cvat.enums.SortingMethod}
* @memberof module:API.cvat.classes.Task
* @instance
* @readonly
*/
get: () => data.sorting_method,
},
_internalData: {
get: () => data,
},
__updatedFields: {
get: () => updatedFields,
_updateTrigger: {
get: () => updateTrigger,
},
}),
);
@ -1720,70 +1828,32 @@
Job.prototype.save.implementation = async function () {
if (this.id) {
const jobData = {};
for (const [field, isUpdated] of Object.entries(this.__updatedFields)) {
if (isUpdated) {
switch (field) {
case 'status':
jobData.status = this.status;
break;
case 'assignee':
jobData.assignee_id = this.assignee ? this.assignee.id : null;
break;
case 'reviewer':
jobData.reviewer_id = this.reviewer ? this.reviewer.id : null;
break;
default:
break;
}
}
const jobData = this._updateTrigger.getUpdated(this);
if (jobData.assignee) {
jobData.assignee = jobData.assignee.id;
}
await serverProxy.jobs.save(this.id, jobData);
this.__updatedFields.reset();
return this;
const data = await serverProxy.jobs.save(this.id, jobData);
this._updateTrigger.reset();
return new Job(data);
}
throw new ArgumentError('Can not save job without and id');
throw new ArgumentError('Could not save job without id');
};
Job.prototype.issues.implementation = async function () {
const result = await serverProxy.jobs.issues(this.id);
const result = await serverProxy.issues.get(this.id);
return result.map((issue) => new Issue(issue));
};
Job.prototype.reviews.implementation = async function () {
const result = await serverProxy.jobs.reviews.get(this.id);
const reviews = result.map((review) => new Review(review));
// try to get not finished review from the local storage
const data = store.get(`job-${this.id}-review`);
if (data) {
reviews.push(new Review(JSON.parse(data)));
}
return reviews;
};
Job.prototype.reviewsSummary.implementation = async function () {
const reviews = await serverProxy.jobs.reviews.get(this.id);
const issues = await serverProxy.jobs.issues(this.id);
const qualities = reviews.map((review) => review.estimated_quality);
const reviewers = reviews.filter((review) => review.reviewer).map((review) => review.reviewer.username);
const assignees = reviews.filter((review) => review.assignee).map((review) => review.assignee.username);
return {
reviews: reviews.length,
average_estimated_quality: qualities.reduce((acc, quality) => acc + quality, 0) / (qualities.length || 1),
issues_unsolved: issues.filter((issue) => !issue.resolved_date).length,
issues_resolved: issues.filter((issue) => issue.resolved_date).length,
assignees: Array.from(new Set(assignees.filter((assignee) => assignee !== null))),
reviewers: Array.from(new Set(reviewers.filter((reviewer) => reviewer !== null))),
};
Job.prototype.openIssue.implementation = async function (issue, message) {
checkObjectType('issue', issue, null, Issue);
checkObjectType('message', message, 'string');
const result = await serverProxy.issues.create({
...issue.serialize(),
message,
});
return new Issue(result);
};
Job.prototype.frames.get.implementation = async function (frame, isPlaying, step) {
@ -1796,27 +1866,32 @@
}
const frameData = await getFrame(
this.task.id,
this.task.dataChunkSize,
this.task.dataChunkType,
this.task.mode,
this.taskId,
this.id,
this.dataChunkSize,
this.dataChunkType,
this.mode,
frame,
this.startFrame,
this.stopFrame,
isPlaying,
step,
this.task.dimension,
this.dimension,
);
return frameData;
};
Job.prototype.frames.ranges.implementation = async function () {
const rangesData = await getRanges(this.task.id);
const rangesData = await getRanges(this.taskId);
return rangesData;
};
Job.prototype.frames.preview.implementation = async function () {
const frameData = await getPreview(this.task.id);
if (this.id === null || this.taskId === null) {
return '';
}
const frameData = await getPreview(this.taskId, this.id);
return frameData;
};
@ -1939,7 +2014,7 @@
};
Job.prototype.annotations.exportDataset.implementation = async function (format, saveImages, customName) {
const result = await exportDataset(this.task, format, customName, saveImages);
const result = await exportDataset(this, format, customName, saveImages);
return result;
};
@ -1969,20 +2044,54 @@
};
Job.prototype.logger.log.implementation = async function (logType, payload, wait) {
const result = await this.task.logger.log(logType, { ...payload, job_id: this.id }, wait);
const result = await loggerStorage.log(logType, { ...payload, task_id: this.taskId, job_id: this.id }, wait);
return result;
};
Job.prototype.predictor.status.implementation = async function () {
const result = await this.task.predictor.status();
return result;
if (!Number.isInteger(this.projectId)) {
throw new DataError('The job must belong to a project to use the feature');
}
const result = await serverProxy.predictor.status(this.projectId);
return {
message: result.message,
progress: result.progress,
projectScore: result.score,
timeRemaining: result.time_remaining,
mediaAmount: result.media_amount,
annotationAmount: result.annotation_amount,
};
};
Job.prototype.predictor.predict.implementation = async function (frame) {
const result = await this.task.predictor.predict(frame);
if (!Number.isInteger(frame) || frame < 0) {
throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`);
}
if (frame < this.startFrame || frame > this.stopFrame) {
throw new ArgumentError(`The frame with number ${frame} is out of the job`);
}
if (!Number.isInteger(this.projectId)) {
throw new DataError('The job must belong to a project to use the feature');
}
const result = await serverProxy.predictor.predict(this.taskId, frame);
return result;
};
Job.prototype.frames.contextImage.implementation = async function (frameId) {
const result = await getContextImage(this.taskId, this.id, frameId);
return result;
};
Job.prototype.close.implementation = function closeTask() {
clearFrames(this.taskId);
closeSession(this);
return this;
};
Task.prototype.close.implementation = function closeTask() {
clearFrames(this.id);
for (const job of this.jobs) {
@ -1997,40 +2106,22 @@
// TODO: Add ability to change an owner and an assignee
if (typeof this.id !== 'undefined') {
// If the task has been already created, we update it
const taskData = {};
for (const [field, isUpdated] of Object.entries(this.__updatedFields)) {
if (isUpdated) {
switch (field) {
case 'assignee':
taskData.assignee_id = this.assignee ? this.assignee.id : null;
break;
case 'name':
taskData.name = this.name;
break;
case 'bug_tracker':
taskData.bug_tracker = this.bugTracker;
break;
case 'subset':
taskData.subset = this.subset;
break;
case 'project_id':
taskData.project_id = this.projectId;
break;
case 'labels':
taskData.labels = [...this._internalData.labels.map((el) => el.toJSON())];
break;
default:
break;
}
}
const taskData = this._updateTrigger.getUpdated(this, {
bugTracker: 'bug_tracker',
projectId: 'project_id',
assignee: 'assignee_id',
});
if (taskData.assignee_id) {
taskData.assignee_id = taskData.assignee_id.id;
}
if (taskData.labels) {
taskData.labels = this._internalData.labels;
taskData.labels = taskData.labels.map((el) => el.toJSON());
}
await serverProxy.tasks.saveTask(this.id, taskData);
this.__updatedFields.reset();
return this;
const data = await serverProxy.tasks.save(this.id, taskData);
this._updateTrigger.reset();
return new Task(data);
}
const taskSpec = {
@ -2061,6 +2152,7 @@
image_quality: this.imageQuality,
use_zip_chunks: this.useZipChunks,
use_cache: this.useCache,
sorting_method: this.sortingMethod,
};
if (typeof this.startFrame !== 'undefined') {
@ -2082,22 +2174,23 @@
taskDataSpec.cloud_storage_id = this.cloudStorageId;
}
const task = await serverProxy.tasks.createTask(taskSpec, taskDataSpec, onUpdate);
const task = await serverProxy.tasks.create(taskSpec, taskDataSpec, onUpdate);
return new Task(task);
};
Task.prototype.delete.implementation = async function () {
const result = await serverProxy.tasks.deleteTask(this.id);
const result = await serverProxy.tasks.delete(this.id);
return result;
};
Task.prototype.export.implementation = async function () {
const result = await serverProxy.tasks.exportTask(this.id);
const result = await serverProxy.tasks.export(this.id);
return result;
};
Task.import.implementation = async function (file) {
const result = await serverProxy.tasks.importTask(file);
// eslint-disable-next-line no-unsanitized/method
const result = await serverProxy.tasks.import(file);
return result;
};
@ -2112,6 +2205,7 @@
const result = await getFrame(
this.id,
null,
this.dataChunkSize,
this.dataChunkType,
this.mode,
@ -2130,6 +2224,10 @@
};
Task.prototype.frames.preview.implementation = async function () {
if (this.id === null) {
return '';
}
const frameData = await getPreview(this.id);
return frameData;
};
@ -2317,9 +2415,4 @@
const result = await serverProxy.predictor.predict(this.id, frame);
return result;
};
Job.prototype.frames.contextImage.implementation = async function (taskId, frameId) {
const result = await getContextImage(taskId, frameId);
return result;
};
})();

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

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -174,10 +174,6 @@
email_verification_required: this.isVerified,
};
}
toJSON() {
return this.serialize();
}
}
module.exports = User;

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -30,8 +30,8 @@ describe('Feature: get annotations', () => {
const annotations10 = await job.annotations.get(10);
expect(Array.isArray(annotations0)).toBeTruthy();
expect(Array.isArray(annotations10)).toBeTruthy();
expect(annotations0).toHaveLength(1);
expect(annotations10).toHaveLength(2);
expect(annotations0).toHaveLength(2);
expect(annotations10).toHaveLength(3);
for (const state of annotations0.concat(annotations10)) {
expect(state).toBeInstanceOf(window.cvat.classes.ObjectState);
}
@ -57,7 +57,57 @@ describe('Feature: get annotations', () => {
expect(job.annotations.get(-1)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
// TODO: Test filter (hasn't been implemented yet)
test('get only ellipses', async () => {
const job = (await window.cvat.jobs.get({ jobID: 101 }))[0];
const annotations = await job.annotations.get(5, false, JSON.parse('[{"and":[{"==":[{"var":"shape"},"ellipse"]}]}]'));
expect(Array.isArray(annotations)).toBeTruthy();
expect(annotations).toHaveLength(1);
expect(annotations[0].shapeType).toBe('ellipse');
});
});
describe('Feature: get interpolated annotations', () => {
test('get interpolated box', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
let annotations = await task.annotations.get(5);
expect(Array.isArray(annotations)).toBeTruthy();
expect(annotations).toHaveLength(2);
const [xtl, ytl, xbr, ybr] = annotations[0].points;
const { rotation } = annotations[0];
expect(rotation).toBe(50);
expect(Math.round(xtl)).toBe(332);
expect(Math.round(ytl)).toBe(519);
expect(Math.round(xbr)).toBe(651);
expect(Math.round(ybr)).toBe(703);
annotations = await task.annotations.get(15);
expect(Array.isArray(annotations)).toBeTruthy();
expect(annotations).toHaveLength(3);
expect(annotations[1].rotation).toBe(40);
expect(annotations[1].shapeType).toBe('rectangle');
annotations = await task.annotations.get(30);
annotations[0].rotation = 20;
await annotations[0].save();
annotations = await task.annotations.get(25);
expect(annotations[0].rotation).toBe(0);
expect(annotations[0].shapeType).toBe('rectangle');
});
test('get interpolated ellipse', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
const annotations = await task.annotations.get(5);
expect(Array.isArray(annotations)).toBeTruthy();
expect(annotations).toHaveLength(2);
expect(annotations[1].shapeType).toBe('ellipse');
const [cx, cy, rightX, topY] = annotations[1].points;
expect(Math.round(cx)).toBe(550);
expect(Math.round(cy)).toBe(550);
expect(Math.round(rightX)).toBe(900);
expect(Math.round(topY)).toBe(150);
});
});
describe('Feature: put annotations', () => {
@ -94,7 +144,29 @@ describe('Feature: put annotations', () => {
shapeType: window.cvat.enums.ObjectShape.RECTANGLE,
points: [0, 0, 100, 100],
occluded: false,
label: job.task.labels[0],
label: job.labels[0],
zOrder: 0,
});
const indexes = await job.annotations.put([state]);
expect(indexes).toBeInstanceOf(Array);
expect(indexes).toHaveLength(1);
annotations = await job.annotations.get(5);
expect(annotations).toHaveLength(length + 1);
});
test('put an ellipse shape to a job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
let annotations = await job.annotations.get(5);
const { length } = annotations;
const state = new window.cvat.classes.ObjectState({
frame: 5,
objectType: window.cvat.enums.ObjectType.SHAPE,
shapeType: window.cvat.enums.ObjectShape.ELLIPSE,
points: [500, 500, 800, 100],
occluded: true,
label: job.labels[0],
zOrder: 0,
});
@ -138,7 +210,7 @@ describe('Feature: put annotations', () => {
shapeType: window.cvat.enums.ObjectShape.RECTANGLE,
points: [0, 0, 100, 100],
occluded: false,
label: job.task.labels[0],
label: job.labels[0],
zOrder: 0,
});
@ -388,7 +460,7 @@ describe('Feature: save annotations', () => {
shapeType: window.cvat.enums.ObjectShape.POLYGON,
points: [0, 0, 100, 0, 100, 50],
occluded: true,
label: job.task.labels[0],
label: job.labels[0],
zOrder: 0,
});
@ -562,7 +634,7 @@ describe('Feature: split annotations', () => {
await task.annotations.split(annotations5[0], 5);
const splitted4 = await task.annotations.get(4);
const splitted5 = (await task.annotations.get(5)).filter((state) => !state.outside);
expect(splitted4[0].clientID).not.toBe(splitted5[0].clientID);
expect(splitted4[1].clientID).not.toBe(splitted5[1].clientID);
});
test('split annotations in a job', async () => {
@ -574,7 +646,7 @@ describe('Feature: split annotations', () => {
await job.annotations.split(annotations5[0], 5);
const splitted4 = await job.annotations.get(4);
const splitted5 = (await job.annotations.get(5)).filter((state) => !state.outside);
expect(splitted4[0].clientID).not.toBe(splitted5[0].clientID);
expect(splitted4[1].clientID).not.toBe(splitted5[1].clientID);
});
test('split on a bad frame', async () => {
@ -702,7 +774,7 @@ describe('Feature: get statistics', () => {
await job.annotations.clear(true);
const statistics = await job.annotations.statistics();
expect(statistics).toBeInstanceOf(window.cvat.classes.Statistics);
expect(statistics.total.total).toBe(512);
expect(statistics.total.total).toBe(1012);
});
});
@ -759,3 +831,34 @@ describe('Feature: select object', () => {
expect(task.annotations.select(annotations, '5', '10')).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
});
describe('Feature: search frame', () => {
test('applying different filters', async () => {
const job = (await window.cvat.jobs.get({ jobID: 102 }))[0];
await job.annotations.clear(true);
let frame = await job.annotations.search(JSON.parse('[{"and":[{"==":[{"var":"type"},"tag"]}]}]'), 495, 994);
expect(frame).toBe(500);
frame = await job.annotations.search(JSON.parse('[{"and":[{"==":[{"var":"type"},"tag"]},{"==":[{"var":"label"},"bicycle"]}]}]'), 495, 994);
expect(frame).toBe(500);
frame = await job.annotations.search(JSON.parse('[{"and":[{"==":[{"var":"type"},"track"]},{"==":[{"var":"label"},"bicycle"]}]}]'), 495, 994);
expect(frame).toBe(null);
frame = await job.annotations.search(JSON.parse('[{"and":[{"==":[{"var":"type"},"shape"]},{"==":[{"var":"shape"},"rectangle"]}]}]'), 495, 994);
expect(frame).toBe(510);
frame = await job.annotations.search(JSON.parse('[{"and":[{"==":[{"var":"type"},"shape"]},{"==":[{"var":"shape"},"rectangle"]}]}]'), 511, 994);
expect(frame).toBe(null);
frame = await job.annotations.search(JSON.parse('[{"and":[{"==":[{"var":"type"},"shape"]},{"==":[{"var":"shape"},"polygon"]}]}]'), 511, 994);
expect(frame).toBe(520);
frame = await job.annotations.search(JSON.parse('[{"and":[{"==":[{"var":"attr.motorcycle.model"},"some text for test"]}]}]'), 495, 994);
expect(frame).toBe(520);
frame = await job.annotations.search(JSON.parse('[{"and":[{"==":[{"var":"attr.motorcycle.model"},"some text for test"]},{"==":[{"var":"shape"},"ellipse"]}]}]'), 495, 994);
expect(frame).toBe(null);
frame = await job.annotations.search(JSON.parse('[{"and":[{"<=":[450,{"var":"width"},550]}]}]'), 540, 994);
expect(frame).toBe(563);
frame = await job.annotations.search(JSON.parse('[{"and":[{"<=":[450,{"var":"width"},550]}]}]'), 588, 994);
expect(frame).toBe(null);
frame = await job.annotations.search(JSON.parse('[{"and":[{">=":[{"var":"width"},500]},{"<=":[{"var":"height"},300]}]}]'), 540, 994);
expect(frame).toBe(575);
});
});

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -36,7 +36,7 @@ describe('Feature: get cloud storages', () => {
expect(cloudStorage.id).toBe(1);
expect(cloudStorage.providerType).toBe('AWS_S3_BUCKET');
expect(cloudStorage.credentialsType).toBe('KEY_SECRET_KEY_PAIR');
expect(cloudStorage.resourceName).toBe('bucket');
expect(cloudStorage.resource).toBe('bucket');
expect(cloudStorage.displayName).toBe('Demonstration bucket');
expect(cloudStorage.manifests).toHaveLength(1);
expect(cloudStorage.manifests[0]).toBe('manifest.jsonl');
@ -61,24 +61,18 @@ describe('Feature: get cloud storages', () => {
});
test('get cloud storages by filters', async () => {
const filters = new Map([
['providerType', 'AWS_S3_BUCKET'],
['resourceName', 'bucket'],
['displayName', 'Demonstration bucket'],
['credentialsType', 'KEY_SECRET_KEY_PAIR'],
['description', 'It is first bucket'],
]);
const result = await window.cvat.cloudStorages.get(Object.fromEntries(filters));
const [cloudStorage] = result;
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(1);
expect(cloudStorage).toBeInstanceOf(CloudStorage);
expect(cloudStorage.id).toBe(1);
filters.forEach((value, key) => {
expect(cloudStorage[key]).toBe(value);
});
const filter = {
and: [
{ '==': [{ var: 'display_name' }, 'Demonstration bucket'] },
{ '==': [{ var: 'resource_name' }, 'bucket'] },
{ '==': [{ var: 'description' }, 'It is first bucket'] },
{ '==': [{ var: 'provider_type' }, 'AWS_S3_BUCKET'] },
{ '==': [{ var: 'credentials_type' }, 'KEY_SECRET_KEY_PAIR'] },
],
};
const result = await window.cvat.cloudStorages.get({ filter: JSON.stringify(filter) });
expect(result).toBeInstanceOf(Array);
});
test('get cloud storage by invalid filters', async () => {

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -25,8 +25,8 @@ describe('Feature: get a list of jobs', () => {
expect(el).toBeInstanceOf(Job);
}
expect(result[0].task.id).toBe(3);
expect(result[0].task).toBe(result[1].task);
expect(result[0].taskId).toBe(3);
expect(result[0].taskId).toBe(result[1].taskId);
});
test('get jobs by an unknown task id', async () => {
@ -89,18 +89,17 @@ describe('Feature: get a list of jobs', () => {
});
describe('Feature: save job', () => {
test('save status of a job', async () => {
let result = await window.cvat.jobs.get({
test('save stage and state of a job', async () => {
const result = await window.cvat.jobs.get({
jobID: 1,
});
result[0].status = 'validation';
await result[0].save();
result[0].stage = 'validation';
result[0].state = 'new';
const newJob = await result[0].save();
result = await window.cvat.jobs.get({
jobID: 1,
});
expect(result[0].status).toBe('validation');
expect(newJob.stage).toBe('validation');
expect(newJob.state).toBe('new');
});
test('save invalid status of a job', async () => {
@ -108,9 +107,11 @@ describe('Feature: save job', () => {
jobID: 1,
});
await result[0].save();
expect(() => {
result[0].status = 'invalid';
result[0].state = 'invalid';
}).toThrow(window.cvat.exceptions.ArgumentError);
expect(() => {
result[0].stage = 'invalid';
}).toThrow(window.cvat.exceptions.ArgumentError);
});
});

@ -1,4 +1,4 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -11,12 +11,11 @@ jest.mock('../../src/server-proxy', () => {
// 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({ withoutTasks: false });
const result = await window.cvat.projects.get();
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(2);
for (const el of result) {
@ -33,8 +32,8 @@ describe('Feature: get projects', () => {
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);
// eslint-disable-next-line no-underscore-dangle
expect(result[0]._internalData.task_ids).toHaveLength(1);
});
test('get a project by an unknown id', async () => {
@ -55,16 +54,12 @@ describe('Feature: get projects', () => {
test('get projects by filters', async () => {
const result = await window.cvat.projects.get({
status: 'completed',
filter: '{"and":[{"==":[{"var":"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');
expect(result).toBeInstanceOf(Array);
});
test('get projects by invalid filters', async () => {
test('get projects by invalid query', async () => {
expect(
window.cvat.projects.get({
unknown: '5',

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -52,39 +52,18 @@ describe('Feature: get a list of tasks', () => {
test('get tasks by filters', async () => {
const result = await window.cvat.tasks.get({
mode: 'interpolation',
filter: '{"and":[{"==":[{"var":"filter"},"interpolation"]}]}',
});
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(3);
for (const el of result) {
expect(el).toBeInstanceOf(Task);
expect(el.mode).toBe('interpolation');
}
expect(result).toBeInstanceOf(Array);
});
test('get tasks by invalid filters', async () => {
test('get tasks by invalid query', async () => {
expect(
window.cvat.tasks.get({
unknown: '5',
}),
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('get task by name, status and mode', async () => {
const result = await window.cvat.tasks.get({
mode: 'interpolation',
status: 'annotation',
name: 'Test Task',
});
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(1);
for (const el of result) {
expect(el).toBeInstanceOf(Task);
expect(el.mode).toBe('interpolation');
expect(el.status).toBe('annotation');
expect(el.name).toBe('Test Task');
}
});
});
describe('Feature: save a task', () => {

@ -55,7 +55,7 @@ const usersDummyData = {
previous: null,
results: [
{
url: 'http://localhost:7000/api/v1/users/1',
url: 'http://localhost:7000/api/users/1',
id: 1,
username: 'admin',
first_name: '',
@ -69,7 +69,7 @@ const usersDummyData = {
date_joined: '2019-05-13T15:33:17.833200+03:00',
},
{
url: 'http://localhost:7000/api/v1/users/2',
url: 'http://localhost:7000/api/users/2',
id: 2,
username: 'bsekache',
first_name: '',
@ -149,18 +149,18 @@ const projectsDummyData = {
previous: null,
results: [
{
url: 'http://192.168.0.139:7000/api/v1/projects/6',
url: 'http://192.168.0.139:7000/api/projects/6',
id: 6,
name: 'Some empty project',
labels: [],
tasks: [],
owner: {
url: 'http://localhost:7000/api/v1/users/2',
url: 'http://localhost:7000/api/users/2',
id: 2,
username: 'bsekache',
},
assignee: {
url: 'http://localhost:7000/api/v1/users/2',
url: 'http://localhost:7000/api/users/2',
id: 2,
username: 'bsekache',
},
@ -170,7 +170,7 @@ const projectsDummyData = {
status: 'annotation',
},
{
url: 'http://192.168.0.139:7000/api/v1/projects/1',
url: 'http://192.168.0.139:7000/api/projects/1',
id: 2,
name: 'Test project with roads',
labels: [
@ -198,13 +198,13 @@ const projectsDummyData = {
],
tasks: [
{
url: 'http://192.168.0.139:7000/api/v1/tasks/2',
url: 'http://192.168.0.139:7000/api/tasks/2',
id: 2,
name: 'road 1',
project_id: 1,
mode: 'interpolation',
owner: {
url: 'http://localhost:7000/api/v1/users/1',
url: 'http://localhost:7000/api/users/1',
id: 1,
username: 'admin',
},
@ -239,11 +239,12 @@ const projectsDummyData = {
stop_frame: 99,
jobs: [
{
url: 'http://192.168.0.139:7000/api/v1/jobs/1',
url: 'http://192.168.0.139:7000/api/jobs/1',
id: 1,
assignee: null,
reviewer: null,
status: 'completed',
stage: 'acceptance',
state: 'completed',
},
],
},
@ -252,11 +253,12 @@ const projectsDummyData = {
stop_frame: 194,
jobs: [
{
url: 'http://192.168.0.139:7000/api/v1/jobs/2',
url: 'http://192.168.0.139:7000/api/jobs/2',
id: 2,
assignee: null,
reviewer: null,
status: 'completed',
stage: 'acceptance',
state: 'completed',
},
],
},
@ -265,11 +267,12 @@ const projectsDummyData = {
stop_frame: 289,
jobs: [
{
url: 'http://192.168.0.139:7000/api/v1/jobs/3',
url: 'http://192.168.0.139:7000/api/jobs/3',
id: 3,
assignee: null,
reviewer: null,
status: 'completed',
stage: 'acceptance',
state: 'completed',
},
],
},
@ -278,11 +281,12 @@ const projectsDummyData = {
stop_frame: 384,
jobs: [
{
url: 'http://192.168.0.139:7000/api/v1/jobs/4',
url: 'http://192.168.0.139:7000/api/jobs/4',
id: 4,
assignee: null,
reviewer: null,
status: 'completed',
stage: 'acceptance',
state: 'completed',
},
],
},
@ -291,11 +295,12 @@ const projectsDummyData = {
stop_frame: 431,
jobs: [
{
url: 'http://192.168.0.139:7000/api/v1/jobs/5',
url: 'http://192.168.0.139:7000/api/jobs/5',
id: 5,
assignee: null,
reviewer: null,
status: 'completed',
stage: 'acceptance',
state: 'completed',
},
],
},
@ -309,7 +314,7 @@ const projectsDummyData = {
},
],
owner: {
url: 'http://localhost:7000/api/v1/users/1',
url: 'http://localhost:7000/api/users/1',
id: 1,
username: 'admin',
},
@ -328,13 +333,13 @@ const tasksDummyData = {
previous: null,
results: [
{
url: 'http://localhost:7000/api/v1/tasks/102',
url: 'http://localhost:7000/api/tasks/102',
id: 102,
name: 'Test',
size: 1,
mode: 'annotation',
owner: {
url: 'http://localhost:7000/api/v1/users/1',
url: 'http://localhost:7000/api/users/1',
id: 1,
username: 'admin',
},
@ -344,6 +349,9 @@ const tasksDummyData = {
updated_date: '2019-09-05T14:04:07.569344Z',
overlap: 0,
segment_size: 0,
dimension: '2d',
data_compressed_chunk_type: 'imageset',
data_chunk_size: 1,
status: 'annotation',
labels: [
{
@ -358,11 +366,12 @@ const tasksDummyData = {
stop_frame: 0,
jobs: [
{
url: 'http://localhost:7000/api/v1/jobs/112',
url: 'http://localhost:7000/api/jobs/112',
id: 112,
assignee: null,
reviewer: null,
status: 'annotation',
stage: 'annotation',
state: 'new',
},
],
},
@ -373,13 +382,13 @@ const tasksDummyData = {
frame_filter: '',
},
{
url: 'http://localhost:7000/api/v1/tasks/100',
url: 'http://localhost:7000/api/tasks/100',
id: 100,
name: 'Image Task',
size: 9,
mode: 'annotation',
owner: {
url: 'http://localhost:7000/api/v1/users/1',
url: 'http://localhost:7000/api/users/1',
id: 1,
username: 'admin',
},
@ -389,6 +398,9 @@ const tasksDummyData = {
updated_date: '2019-07-16T15:51:29.142871+03:00',
overlap: 0,
segment_size: 0,
dimension: '2d',
data_compressed_chunk_type: 'imageset',
data_chunk_size: 1,
status: 'annotation',
labels: [
{
@ -408,11 +420,12 @@ const tasksDummyData = {
stop_frame: 8,
jobs: [
{
url: 'http://localhost:7000/api/v1/jobs/100',
url: 'http://localhost:7000/api/jobs/100',
id: 100,
assignee: null,
reviewer: null,
status: 'annotation',
stage: 'annotation',
state: 'new',
},
],
},
@ -423,13 +436,13 @@ const tasksDummyData = {
frame_filter: '',
},
{
url: 'http://localhost:7000/api/v1/tasks/10',
url: 'http://localhost:7000/api/tasks/10',
id: 101,
name: 'Video Task',
size: 5002,
mode: 'interpolation',
owner: {
url: 'http://localhost:7000/api/v1/users/1',
url: 'http://localhost:7000/api/users/1',
id: 1,
username: 'admin',
},
@ -439,6 +452,9 @@ const tasksDummyData = {
updated_date: '2019-07-12T16:43:58.904892+03:00',
overlap: 5,
segment_size: 500,
dimension: '2d',
data_compressed_chunk_type: 'imageset',
data_chunk_size: 1,
status: 'annotation',
labels: [
{
@ -612,11 +628,12 @@ const tasksDummyData = {
stop_frame: 499,
jobs: [
{
url: 'http://localhost:7000/api/v1/jobs/10',
url: 'http://localhost:7000/api/jobs/10',
id: 101,
assignee: null,
reviewer: null,
status: 'annotation',
stage: 'annotation',
state: 'new',
},
],
},
@ -625,11 +642,12 @@ const tasksDummyData = {
stop_frame: 994,
jobs: [
{
url: 'http://localhost:7000/api/v1/jobs/11',
url: 'http://localhost:7000/api/jobs/11',
id: 102,
assignee: null,
reviewer: null,
status: 'annotation',
stage: 'annotation',
state: 'new',
},
],
},
@ -638,11 +656,12 @@ const tasksDummyData = {
stop_frame: 1489,
jobs: [
{
url: 'http://localhost:7000/api/v1/jobs/12',
url: 'http://localhost:7000/api/jobs/12',
id: 103,
assignee: null,
reviewer: null,
status: 'annotation',
stage: 'annotation',
state: 'new',
},
],
},
@ -651,11 +670,12 @@ const tasksDummyData = {
stop_frame: 1984,
jobs: [
{
url: 'http://localhost:7000/api/v1/jobs/13',
url: 'http://localhost:7000/api/jobs/13',
id: 104,
assignee: null,
reviewer: null,
status: 'annotation',
stage: 'annotation',
state: 'new',
},
],
},
@ -664,11 +684,12 @@ const tasksDummyData = {
stop_frame: 2479,
jobs: [
{
url: 'http://localhost:7000/api/v1/jobs/14',
url: 'http://localhost:7000/api/jobs/14',
id: 105,
assignee: null,
reviewer: null,
status: 'annotation',
stage: 'annotation',
state: 'new',
},
],
},
@ -677,11 +698,12 @@ const tasksDummyData = {
stop_frame: 2974,
jobs: [
{
url: 'http://localhost:7000/api/v1/jobs/15',
url: 'http://localhost:7000/api/jobs/15',
id: 106,
assignee: null,
reviewer: null,
status: 'annotation',
stage: 'annotation',
state: 'new',
},
],
},
@ -690,11 +712,12 @@ const tasksDummyData = {
stop_frame: 3469,
jobs: [
{
url: 'http://localhost:7000/api/v1/jobs/16',
url: 'http://localhost:7000/api/jobs/16',
id: 107,
assignee: null,
reviewer: null,
status: 'annotation',
stage: 'annotation',
state: 'new',
},
],
},
@ -703,11 +726,12 @@ const tasksDummyData = {
stop_frame: 3964,
jobs: [
{
url: 'http://localhost:7000/api/v1/jobs/17',
url: 'http://localhost:7000/api/jobs/17',
id: 108,
assignee: null,
reviewer: null,
status: 'annotation',
stage: 'annotation',
state: 'new',
},
],
},
@ -716,11 +740,12 @@ const tasksDummyData = {
stop_frame: 4459,
jobs: [
{
url: 'http://localhost:7000/api/v1/jobs/18',
url: 'http://localhost:7000/api/jobs/18',
id: 109,
assignee: null,
reviewer: null,
status: 'annotation',
stage: 'annotation',
state: 'new',
},
],
},
@ -729,11 +754,12 @@ const tasksDummyData = {
stop_frame: 4954,
jobs: [
{
url: 'http://localhost:7000/api/v1/jobs/19',
url: 'http://localhost:7000/api/jobs/19',
id: 110,
assignee: null,
reviewer: null,
status: 'annotation',
stage: 'annotation',
state: 'new',
},
],
},
@ -742,11 +768,12 @@ const tasksDummyData = {
stop_frame: 5001,
jobs: [
{
url: 'http://localhost:7000/api/v1/jobs/20',
url: 'http://localhost:7000/api/jobs/20',
id: 111,
assignee: null,
reviewer: null,
status: 'annotation',
stage: 'annotation',
state: 'new',
},
],
},
@ -757,13 +784,13 @@ const tasksDummyData = {
frame_filter: '',
},
{
url: 'http://localhost:7000/api/v1/tasks/3',
url: 'http://localhost:7000/api/tasks/3',
id: 3,
name: 'Test Task',
size: 5002,
mode: 'interpolation',
owner: {
url: 'http://localhost:7000/api/v1/users/2',
url: 'http://localhost:7000/api/users/2',
id: 2,
username: 'bsekache',
},
@ -773,7 +800,9 @@ const tasksDummyData = {
updated_date: '2019-05-16T13:08:00.621797+03:00',
overlap: 5,
segment_size: 5000,
flipped: false,
dimension: '2d',
data_compressed_chunk_type: 'imageset',
data_chunk_size: 1,
status: 'annotation',
labels: [
{
@ -947,11 +976,12 @@ const tasksDummyData = {
stop_frame: 4999,
jobs: [
{
url: 'http://localhost:7000/api/v1/jobs/3',
url: 'http://localhost:7000/api/jobs/3',
id: 3,
assignee: null,
reviewer: null,
status: 'annotation',
stage: 'annotation',
state: 'new',
},
],
},
@ -960,11 +990,12 @@ const tasksDummyData = {
stop_frame: 5001,
jobs: [
{
url: 'http://localhost:7000/api/v1/jobs/4',
url: 'http://localhost:7000/api/jobs/4',
id: 4,
assignee: null,
reviewer: null,
status: 'annotation',
stage: 'annotation',
state: 'new',
},
],
},
@ -972,13 +1003,13 @@ const tasksDummyData = {
image_quality: 50,
},
{
url: 'http://localhost:7000/api/v1/tasks/2',
url: 'http://localhost:7000/api/tasks/2',
id: 2,
name: 'Video',
size: 75,
mode: 'interpolation',
owner: {
url: 'http://localhost:7000/api/v1/users/1',
url: 'http://localhost:7000/api/users/1',
id: 1,
username: 'admin',
},
@ -989,7 +1020,9 @@ const tasksDummyData = {
updated_date: '2019-05-15T16:58:27.992785+03:00',
overlap: 5,
segment_size: 0,
flipped: false,
dimension: '2d',
data_compressed_chunk_type: 'imageset',
data_chunk_size: 1,
status: 'annotation',
labels: [
{
@ -1163,11 +1196,12 @@ const tasksDummyData = {
stop_frame: 74,
jobs: [
{
url: 'http://localhost:7000/api/v1/jobs/2',
url: 'http://localhost:7000/api/jobs/2',
id: 2,
assignee: null,
reviewer: null,
status: 'annotation',
stage: 'annotation',
state: 'new',
},
],
},
@ -1175,13 +1209,13 @@ const tasksDummyData = {
image_quality: 50,
},
{
url: 'http://localhost:7000/api/v1/tasks/1',
url: 'http://localhost:7000/api/tasks/1',
id: 1,
name: 'Labels Set',
size: 9,
mode: 'annotation',
owner: {
url: 'http://localhost:7000/api/v1/users/1',
url: 'http://localhost:7000/api/users/1',
id: 1,
username: 'admin',
},
@ -1191,7 +1225,9 @@ const tasksDummyData = {
updated_date: '2019-05-15T11:20:55.770587+03:00',
overlap: 0,
segment_size: 0,
flipped: false,
dimension: '2d',
data_compressed_chunk_type: 'imageset',
data_chunk_size: 1,
status: 'annotation',
labels: [
{
@ -1365,11 +1401,12 @@ const tasksDummyData = {
stop_frame: 8,
jobs: [
{
url: 'http://localhost:7000/api/v1/jobs/1',
url: 'http://localhost:7000/api/jobs/1',
id: 1,
assignee: null,
reviewer: null,
status: 'annotation',
stage: "annotation",
state: "new",
},
],
},
@ -1418,6 +1455,74 @@ const taskAnnotationsDummyData = {
},
],
},
102: {
version: 21,
tags: [{
id: 1,
frame: 500,
label_id: 22,
group: 0,
attributes: [{
spec_id: 13,
value: 'woman',
}, {
spec_id: 14,
value: 'false',
}],
}],
shapes: [{
type: 'rectangle',
occluded: false,
z_order: 1,
points: [557.7890625, 276.2216796875, 907.1888732910156, 695.5014038085938],
id: 2,
frame: 510,
label_id: 21,
group: 0,
attributes: [],
}, {
type: 'polygon',
occluded: false,
z_order: 2,
points: [0, 0, 500, 500, 1000, 0],
id: 3,
frame: 520,
label_id: 23,
group: 0,
attributes: [{ spec_id: 15, value: 'some text for test' }],
}],
tracks: [
{
id: 4,
frame: 550,
label_id: 24,
group: 0,
shapes: [
{
type: 'rectangle',
occluded: true,
z_order: 2,
points: [100, 100, 500, 500],
id: 1,
frame: 550,
outside: false,
attributes: [],
},
{
type: 'rectangle',
occluded: false,
z_order: 2,
points: [100, 100, 700, 300],
id: 3,
frame: 600,
outside: false,
attributes: [],
},
],
attributes: [],
},
],
},
101: {
version: 21,
tags: [],
@ -1740,6 +1845,7 @@ const taskAnnotationsDummyData = {
occluded: false,
z_order: 1,
points: [425.58984375, 540.298828125, 755.9765625, 745.6328125],
rotation: 0,
id: 379,
frame: 0,
outside: false,
@ -1759,6 +1865,7 @@ const taskAnnotationsDummyData = {
occluded: false,
z_order: 1,
points: [238.8000000000011, 498.6000000000022, 546.01171875, 660.720703125],
rotation: 100,
id: 380,
frame: 10,
outside: false,
@ -1769,6 +1876,7 @@ const taskAnnotationsDummyData = {
occluded: false,
z_order: 1,
points: [13.3955078125, 447.650390625, 320.6072265624989, 609.7710937499978],
rotation: 340,
id: 381,
frame: 20,
outside: false,
@ -1790,6 +1898,38 @@ const taskAnnotationsDummyData = {
},
],
},
{
id: 61,
frame: 0,
label_id: 19,
group: 0,
shapes: [
{
type: 'ellipse',
occluded: false,
z_order: 1,
points: [500, 500, 800, 100],
rotation: 0,
id: 611,
frame: 0,
outside: false,
attributes: [],
},
{
type: 'ellipse',
occluded: false,
z_order: 1,
points: [600, 600, 1000, 200],
rotation: 0,
id: 612,
frame: 10,
outside: false,
attributes: [],
},
],
attributes: [],
},
],
},
100: {
@ -2548,14 +2688,35 @@ const frameMetaDummyData = {
};
const cloudStoragesDummyData = {
count: 2,
count: 3,
next: null,
previous: null,
results: [
{
id: 3,
owner: {
url: 'http://localhost:7000/api/users/1',
id: 1,
username: 'maya',
first_name: '',
last_name: ''
},
manifests: [
'manifest.jsonl'
],
provider_type: 'GOOGLE_CLOUD_STORAGE',
resource: 'gcsbucket',
display_name: 'Demo GCS',
created_date: '2021-09-01T09:29:47.094244Z',
updated_date: '2021-09-01T09:29:47.103264Z',
credentials_type: 'KEY_FILE_PATH',
specific_attributes: '',
description: 'It is first google cloud storage'
},
{
id: 2,
owner: {
url: 'http://localhost:7000/api/v1/users/1',
url: 'http://localhost:7000/api/users/1',
id: 1,
username: 'maya',
first_name: '',
@ -2576,7 +2737,7 @@ const cloudStoragesDummyData = {
{
id: 1,
owner: {
url: 'http://localhost:7000/api/v1/users/1',
url: 'http://localhost:7000/api/users/1',
id: 1,
username: 'maya',
first_name: '',

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -76,7 +76,7 @@ class ServerProxy {
}
async function getProjects(filter = '') {
const queries = QueryStringToJSON(filter, ['without_tasks']);
const queries = QueryStringToJSON(filter);
const result = projectsDummyData.results.filter((x) => {
for (const key in queries) {
if (Object.prototype.hasOwnProperty.call(queries, key)) {
@ -97,8 +97,8 @@ class ServerProxy {
const object = projectsDummyData.results.filter((project) => project.id === id)[0];
for (const prop in projectData) {
if (
Object.prototype.hasOwnProperty.call(projectData, prop)
&& Object.prototype.hasOwnProperty.call(object, prop)
Object.prototype.hasOwnProperty.call(projectData, prop) &&
Object.prototype.hasOwnProperty.call(object, prop)
) {
if (prop === 'labels') {
object[prop] = projectData[prop].filter((label) => !label.deleted);
@ -113,7 +113,7 @@ class ServerProxy {
const id = Math.max(...projectsDummyData.results.map((el) => el.id)) + 1;
projectsDummyData.results.push({
id,
url: `http://localhost:7000/api/v1/projects/${id}`,
url: `http://localhost:7000/api/projects/${id}`,
name: projectData.name,
owner: 1,
assignee: null,
@ -160,8 +160,8 @@ class ServerProxy {
const object = tasksDummyData.results.filter((task) => task.id === id)[0];
for (const prop in taskData) {
if (
Object.prototype.hasOwnProperty.call(taskData, prop)
&& Object.prototype.hasOwnProperty.call(object, prop)
Object.prototype.hasOwnProperty.call(taskData, prop) &&
Object.prototype.hasOwnProperty.call(object, prop)
) {
if (prop === 'labels') {
object[prop] = taskData[prop].filter((label) => !label.deleted);
@ -170,13 +170,16 @@ class ServerProxy {
}
}
}
const [updatedTask] = await getTasks({ id });
return updatedTask;
}
async function createTask(taskData) {
const id = Math.max(...tasksDummyData.results.map((el) => el.id)) + 1;
tasksDummyData.results.push({
id,
url: `http://localhost:7000/api/v1/tasks/${id}`,
url: `http://localhost:7000/api/tasks/${id}`,
name: taskData.name,
project_id: taskData.project_id || null,
size: 5000,
@ -209,7 +212,8 @@ class ServerProxy {
}
}
async function getJob(jobID) {
async function getJobs(filter = {}) {
const id = filter.id || null;
const jobs = tasksDummyData.results
.reduce((acc, task) => {
for (const segment of task.segments) {
@ -218,6 +222,12 @@ class ServerProxy {
copy.start_frame = segment.start_frame;
copy.stop_frame = segment.stop_frame;
copy.task_id = task.id;
copy.dimension = task.dimension;
copy.data_compressed_chunk_type = task.data_compressed_chunk_type;
copy.data_chunk_size = task.data_chunk_size;
copy.bug_tracker = task.bug_tracker;
copy.mode = task.mode;
copy.labels = task.labels;
acc.push(copy);
}
@ -225,7 +235,7 @@ class ServerProxy {
return acc;
}, [])
.filter((job) => job.id === jobID);
.filter((job) => job.id === id);
return (
jobs[0] || {
@ -249,12 +259,14 @@ class ServerProxy {
for (const prop in jobData) {
if (
Object.prototype.hasOwnProperty.call(jobData, prop)
&& Object.prototype.hasOwnProperty.call(object, prop)
Object.prototype.hasOwnProperty.call(jobData, prop) &&
Object.prototype.hasOwnProperty.call(object, prop)
) {
object[prop] = jobData[prop];
}
}
return getJobs({ id });
}
async function getUsers() {
@ -339,8 +351,8 @@ class ServerProxy {
if (cloudStorage) {
for (const prop in cloudStorageData) {
if (
Object.prototype.hasOwnProperty.call(cloudStorageData, prop)
&& Object.prototype.hasOwnProperty.call(cloudStorage, prop)
Object.prototype.hasOwnProperty.call(cloudStorageData, prop) &&
Object.prototype.hasOwnProperty.call(cloudStorage, prop)
) {
cloudStorage[prop] = cloudStorageData[prop];
}
@ -375,7 +387,6 @@ class ServerProxy {
}
}
Object.defineProperties(
this,
Object.freeze({
@ -403,17 +414,17 @@ class ServerProxy {
tasks: {
value: Object.freeze({
getTasks,
saveTask,
createTask,
deleteTask,
get: getTasks,
save: saveTask,
create: createTask,
delete: deleteTask,
}),
writable: false,
},
jobs: {
value: Object.freeze({
get: getJob,
get: getJobs,
save: saveJob,
}),
writable: false,

@ -1,12 +1,12 @@
{
"name": "cvat-ui",
"version": "1.25.0",
"version": "1.36.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "cvat-ui",
"version": "1.25.0",
"version": "1.36.0",
"license": "MIT",
"dependencies": {
"@ant-design/icons": "^4.6.3",
@ -22,7 +22,7 @@
"@types/react-share": "^3.0.3",
"@types/redux-logger": "^3.0.9",
"@types/resize-observer-browser": "^0.1.6",
"antd": "^4.16.13",
"antd": "^4.17.0",
"copy-to-clipboard": "^3.3.1",
"cvat-canvas": "file:../cvat-canvas",
"cvat-canvas3d": "file:../cvat-canvas3d",
@ -35,7 +35,7 @@
"platform": "^1.3.6",
"prop-types": "^15.7.2",
"react": "^16.14.0",
"react-awesome-query-builder": "^4.4.2",
"react-awesome-query-builder": "^4.5.1",
"react-color": "^2.19.3",
"react-cookie": "^4.0.3",
"react-dom": "^16.14.0",
@ -45,6 +45,7 @@
"react-router": "^5.1.0",
"react-router-dom": "^5.1.0",
"react-share": "^4.4.0",
"react-sortable-hoc": "^2.0.0",
"redux": "^4.1.1",
"redux-devtools-extension": "^2.13.9",
"redux-logger": "^3.0.6",
@ -53,16 +54,17 @@
"devDependencies": {}
},
"../cvat-canvas": {
"version": "2.8.0",
"version": "2.13.1",
"license": "MIT",
"dependencies": {
"@types/polylabel": "^1.0.5",
"polylabel": "^1.1.0",
"svg.draggable.js": "2.2.2",
"svg.draw.js": "^2.0.4",
"svg.js": "2.7.1",
"svg.resize.js": "1.4.3",
"svg.select.js": "3.0.1"
},
"devDependencies": {}
}
},
"../cvat-canvas3d": {
"version": "0.0.1",
@ -75,13 +77,13 @@
"devDependencies": {}
},
"../cvat-core": {
"version": "3.16.1",
"version": "4.2.1",
"license": "MIT",
"dependencies": {
"axios": "^0.21.4",
"browser-or-node": "^1.2.1",
"cvat-data": "../cvat-data",
"detect-browser": "^5.2.0",
"detect-browser": "^5.2.1",
"error-stack-parser": "^2.0.2",
"form-data": "^2.5.0",
"jest-config": "^26.6.3",
@ -90,7 +92,7 @@
"platform": "^1.3.5",
"quickhull": "^1.0.3",
"store": "^2.0.12",
"worker-loader": "^2.0.0"
"tus-js-client": "^2.3.0"
},
"devDependencies": {
"coveralls": "^3.0.5",
@ -854,50 +856,51 @@
}
},
"node_modules/antd": {
"version": "4.16.13",
"resolved": "https://registry.npmjs.org/antd/-/antd-4.16.13.tgz",
"integrity": "sha512-EMPD3fzKe7oayx9keD/GA1oKatcx7j5CGlkJj5eLS0/eEDDEkxVj3DFmKOPuHYt4BK7ltTzMFS+quSTmqUXPiw==",
"version": "4.17.0",
"resolved": "https://registry.npmjs.org/antd/-/antd-4.17.0.tgz",
"integrity": "sha512-V2xBGzBK+s2Iy7Re5JOcOBtAvaZtJ9t7R1fFOP51T6ynfSvJqaRtG4DjBu7i9inhXkCzrt7eGcX3vMqLCqXV8g==",
"dependencies": {
"@ant-design/colors": "^6.0.0",
"@ant-design/icons": "^4.6.3",
"@ant-design/icons": "^4.7.0",
"@ant-design/react-slick": "~0.28.1",
"@babel/runtime": "^7.12.5",
"@ctrl/tinycolor": "^3.4.0",
"array-tree-filter": "^2.1.0",
"classnames": "^2.2.6",
"copy-to-clipboard": "^3.2.0",
"lodash": "^4.17.21",
"moment": "^2.25.3",
"rc-cascader": "~1.4.0",
"rc-cascader": "~2.1.0",
"rc-checkbox": "~2.3.0",
"rc-collapse": "~3.1.0",
"rc-dialog": "~8.6.0",
"rc-drawer": "~4.3.0",
"rc-drawer": "~4.4.2",
"rc-dropdown": "~3.2.0",
"rc-field-form": "~1.20.0",
"rc-field-form": "~1.21.0",
"rc-image": "~5.2.5",
"rc-input-number": "~7.1.0",
"rc-input-number": "~7.3.0",
"rc-mentions": "~1.6.1",
"rc-menu": "~9.0.12",
"rc-motion": "^2.4.0",
"rc-motion": "^2.4.4",
"rc-notification": "~4.5.7",
"rc-pagination": "~3.1.9",
"rc-picker": "~2.5.10",
"rc-picker": "~2.5.17",
"rc-progress": "~3.1.0",
"rc-rate": "~2.9.0",
"rc-resize-observer": "^1.0.0",
"rc-select": "~12.1.6",
"rc-slider": "~9.7.1",
"rc-select": "~13.1.0-alpha.0",
"rc-slider": "~9.7.4",
"rc-steps": "~4.1.0",
"rc-switch": "~3.2.0",
"rc-table": "~7.15.1",
"rc-table": "~7.19.0",
"rc-tabs": "~11.10.0",
"rc-textarea": "~0.3.0",
"rc-tooltip": "~5.1.1",
"rc-tree": "~4.2.1",
"rc-tree-select": "~4.3.0",
"rc-tree": "~5.2.0",
"rc-tree-select": "~4.6.0",
"rc-trigger": "^5.2.10",
"rc-upload": "~4.3.0",
"rc-util": "^5.13.1",
"rc-util": "^5.14.0",
"scroll-into-view-if-needed": "^2.2.25"
},
"funding": {
@ -925,12 +928,13 @@
}
},
"node_modules/antd/node_modules/rc-cascader": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-1.4.3.tgz",
"integrity": "sha512-Q4l9Mv8aaISJ+giVnM9IaXxDeMqHUGLvi4F+LksS6pHlaKlN4awop/L+IMjIXpL+ug/ojaCyv/ixcVopJYYCVA==",
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-2.1.5.tgz",
"integrity": "sha512-FiGPfSxKmSft2CT2XSr6HeKihqcxM+1ozmH6FGXTDthVNNvV0ai82CA6l30iPmMmlflwDfSm/623qkekqNq4BQ==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"array-tree-filter": "^2.1.0",
"rc-tree-select": "~4.6.0",
"rc-trigger": "^5.0.4",
"rc-util": "^5.0.1",
"warning": "^4.0.1"
@ -985,9 +989,9 @@
}
},
"node_modules/antd/node_modules/rc-drawer": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-4.3.1.tgz",
"integrity": "sha512-GMfFy4maqxS9faYXEhQ+0cA1xtkddEQzraf6SAdzWbn444DrrLogwYPk1NXSpdXjLCLxgxOj9MYtyYG42JsfXg==",
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-4.4.3.tgz",
"integrity": "sha512-FYztwRs3uXnFOIf1hLvFxIQP9MiZJA+0w+Os8dfDh/90X7z/HqP/Yg+noLCIeHEbKln1Tqelv8ymCAN24zPcfQ==",
"dependencies": {
"@babel/runtime": "^7.10.1",
"classnames": "^2.2.6",
@ -1013,12 +1017,12 @@
}
},
"node_modules/antd/node_modules/rc-field-form": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-1.20.1.tgz",
"integrity": "sha512-f64KEZop7zSlrG4ef/PLlH12SLn6iHDQ3sTG+RfKBM45hikwV1i8qMf53xoX12NvXXWg1VwchggX/FSso4bWaA==",
"version": "1.21.2",
"resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-1.21.2.tgz",
"integrity": "sha512-LR/bURt/Tf5g39mb0wtMtQuWn42d/7kEzpzlC5fNC7yaRVmLTtlPP4sBBlaViETM9uZQKLoaB0Pt9Mubhm9gow==",
"dependencies": {
"@babel/runtime": "^7.8.4",
"async-validator": "^3.0.3",
"async-validator": "^4.0.2",
"rc-util": "^5.8.0"
},
"engines": {
@ -1045,9 +1049,9 @@
}
},
"node_modules/antd/node_modules/rc-input-number": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-7.1.4.tgz",
"integrity": "sha512-EG4iqkqyqzLRu/Dq+fw2od7nlgvXLEatE+J6uhi3HXE1qlM3C7L6a7o/hL9Ly9nimkES2IeQoj3Qda3I0izj3Q==",
"version": "7.3.4",
"resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-7.3.4.tgz",
"integrity": "sha512-W9uqSzuvJUnz8H8vsVY4kx+yK51SsAxNTwr8SNH4G3XqQNocLVmKIibKFRjocnYX1RDHMND9FFbgj2h7E7nvGA==",
"dependencies": {
"@babel/runtime": "^7.10.1",
"classnames": "^2.2.5",
@ -1221,9 +1225,9 @@
}
},
"node_modules/antd/node_modules/rc-select": {
"version": "12.1.13",
"resolved": "https://registry.npmjs.org/rc-select/-/rc-select-12.1.13.tgz",
"integrity": "sha512-cPI+aesP6dgCAaey4t4upDbEukJe+XN0DK6oO/6flcCX5o28o7KNZD7JAiVtC/6fCwqwI/kSs7S/43dvHmBl+A==",
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/rc-select/-/rc-select-13.1.1.tgz",
"integrity": "sha512-Oy4L27x5QgGR8902pw0bJVjrTWFnKPKvdLHzJl5pjiA+jM1hpzDfLGg/bY2ntk5ElxxQKZUwbFKUeqfCQU7SrQ==",
"dependencies": {
"@babel/runtime": "^7.10.1",
"classnames": "2.x",
@ -1257,9 +1261,9 @@
}
},
"node_modules/antd/node_modules/rc-select/node_modules/rc-virtual-list": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.4.1.tgz",
"integrity": "sha512-YexJy+Cx8qjnQdV8+0JBeM65VF2kvO9lnsfrIvHsL3lIH1adMZ85HqmePGUzKkKMZC+CRAJc2K4g2iJS1dOjPw==",
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.4.2.tgz",
"integrity": "sha512-OyVrrPvvFcHvV0ssz5EDZ+7Rf5qLat/+mmujjchNw5FfbJWNDwkpQ99EcVE6+FtNRmX9wFa1LGNpZLUTvp/4GQ==",
"dependencies": {
"classnames": "^2.2.6",
"rc-resize-observer": "^1.0.0",
@ -1274,14 +1278,14 @@
}
},
"node_modules/antd/node_modules/rc-slider": {
"version": "9.7.2",
"resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-9.7.2.tgz",
"integrity": "sha512-mVaLRpDo6otasBs6yVnG02ykI3K6hIrLTNfT5eyaqduFv95UODI9PDS6fWuVVehVpdS4ENgOSwsTjrPVun+k9g==",
"version": "9.7.5",
"resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-9.7.5.tgz",
"integrity": "sha512-LV/MWcXFjco1epPbdw1JlLXlTgmWpB9/Y/P2yinf8Pg3wElHxA9uajN21lJiWtZjf5SCUekfSP6QMJfDo4t1hg==",
"dependencies": {
"@babel/runtime": "^7.10.1",
"classnames": "^2.2.5",
"rc-tooltip": "^5.0.1",
"rc-util": "^5.0.0",
"rc-util": "^5.16.1",
"shallowequal": "^1.1.0"
},
"engines": {
@ -1292,6 +1296,20 @@
"react-dom": ">=16.9.0"
}
},
"node_modules/antd/node_modules/rc-slider/node_modules/rc-util": {
"version": "5.16.1",
"resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.16.1.tgz",
"integrity": "sha512-kSCyytvdb3aRxQacS/71ta6c+kBWvM1v8/2h9d/HaNWauc3qB8pLnF20PJ8NajkNN8gb+rR1l0eWO+D4Pz+LLQ==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"react-is": "^16.12.0",
"shallowequal": "^1.1.0"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/antd/node_modules/rc-steps": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-4.1.4.tgz",
@ -1324,14 +1342,14 @@
}
},
"node_modules/antd/node_modules/rc-table": {
"version": "7.15.2",
"resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.15.2.tgz",
"integrity": "sha512-TAs7kCpIZwc2mtvD8CMrXSM6TqJDUsy0rUEV1YgRru33T8bjtAtc+9xW/KC1VWROJlHSpU0R0kXjFs9h/6+IzQ==",
"version": "7.19.2",
"resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.19.2.tgz",
"integrity": "sha512-NdpnoM50MK02H5/hGOsObfxCvGFUG5cHB9turE5BKJ81T5Ycbq193w5tLhnpILXe//Oanzr47MdMxkUnVGP+qg==",
"dependencies": {
"@babel/runtime": "^7.10.1",
"classnames": "^2.2.5",
"rc-resize-observer": "^1.0.0",
"rc-util": "^5.13.0",
"rc-util": "^5.14.0",
"shallowequal": "^1.1.0"
},
"engines": {
@ -1391,15 +1409,15 @@
}
},
"node_modules/antd/node_modules/rc-tree": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-4.2.2.tgz",
"integrity": "sha512-V1hkJt092VrOVjNyfj5IYbZKRMHxWihZarvA5hPL/eqm7o2+0SNkeidFYm7LVVBrAKBpOpa0l8xt04uiqOd+6w==",
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.2.2.tgz",
"integrity": "sha512-ZQPGi5rGmipXvSUqeMbh0Rm0Cn2zFVWQFvS3sinH+lis5VNCChkFs2dAFpWZnb9/d/SZPeMfYG/x2XFq/q3UTA==",
"dependencies": {
"@babel/runtime": "^7.10.1",
"classnames": "2.x",
"rc-motion": "^2.0.1",
"rc-util": "^5.0.0",
"rc-virtual-list": "^3.0.1"
"rc-virtual-list": "^3.4.1"
},
"engines": {
"node": ">=10.x"
@ -1410,15 +1428,15 @@
}
},
"node_modules/antd/node_modules/rc-tree-select": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-4.3.3.tgz",
"integrity": "sha512-0tilOHLJA6p+TNg4kD559XnDX3PTEYuoSF7m7ryzFLAYvdEEPtjn0QZc5z6L0sMKBiBlj8a2kf0auw8XyHU3lA==",
"version": "4.6.3",
"resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-4.6.3.tgz",
"integrity": "sha512-VymfystOnW8EfoWaWehgB8zpYKgRZf4ILu9KHf7FJZVZ/1dnBEHDqg1bBi43/1BYLwYFKSKKSjkYyNYntWJM4A==",
"dependencies": {
"@babel/runtime": "^7.10.1",
"classnames": "2.x",
"rc-select": "^12.0.0",
"rc-tree": "^4.0.0",
"rc-util": "^5.0.5"
"rc-select": "~13.1.0-alpha.0",
"rc-tree": "~5.2.0",
"rc-util": "^5.7.0"
},
"peerDependencies": {
"react": "*",
@ -1426,9 +1444,9 @@
}
},
"node_modules/antd/node_modules/rc-tree/node_modules/rc-virtual-list": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.4.1.tgz",
"integrity": "sha512-YexJy+Cx8qjnQdV8+0JBeM65VF2kvO9lnsfrIvHsL3lIH1adMZ85HqmePGUzKkKMZC+CRAJc2K4g2iJS1dOjPw==",
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.4.2.tgz",
"integrity": "sha512-OyVrrPvvFcHvV0ssz5EDZ+7Rf5qLat/+mmujjchNw5FfbJWNDwkpQ99EcVE6+FtNRmX9wFa1LGNpZLUTvp/4GQ==",
"dependencies": {
"classnames": "^2.2.6",
"rc-resize-observer": "^1.0.0",
@ -1632,9 +1650,9 @@
"peer": true
},
"node_modules/async-validator": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-3.5.2.tgz",
"integrity": "sha512-8eLCg00W9pIRZSB781UUX/H6Oskmm8xloZfr09lz5bikRpBVDlJ3hRVuxxP1SxcwsEYfJ4IU8Q19Y8/893r3rQ=="
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.0.7.tgz",
"integrity": "sha512-Pj2IR7u8hmUEDOwB++su6baaRi+QvsgajuFB9j95foM1N2gy5HM4z60hfusIO0fBPG5uLAEl6yCJr1jNSVugEQ=="
},
"node_modules/atob": {
"version": "2.1.2",
@ -4678,6 +4696,21 @@
"react": "^16.3.0 || ^17"
}
},
"node_modules/react-sortable-hoc": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-sortable-hoc/-/react-sortable-hoc-2.0.0.tgz",
"integrity": "sha512-JZUw7hBsAHXK7PTyErJyI7SopSBFRcFHDjWW5SWjcugY0i6iH7f+eJkY8cJmGMlZ1C9xz1J3Vjz0plFpavVeRg==",
"dependencies": {
"@babel/runtime": "^7.2.0",
"invariant": "^2.2.4",
"prop-types": "^15.5.7"
},
"peerDependencies": {
"prop-types": "^15.5.7",
"react": "^16.3.0 || ^17.0.0",
"react-dom": "^16.3.0 || ^17.0.0"
}
},
"node_modules/reactcss": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
@ -6675,50 +6708,51 @@
"requires": {}
},
"antd": {
"version": "4.16.13",
"resolved": "https://registry.npmjs.org/antd/-/antd-4.16.13.tgz",
"integrity": "sha512-EMPD3fzKe7oayx9keD/GA1oKatcx7j5CGlkJj5eLS0/eEDDEkxVj3DFmKOPuHYt4BK7ltTzMFS+quSTmqUXPiw==",
"version": "4.17.0",
"resolved": "https://registry.npmjs.org/antd/-/antd-4.17.0.tgz",
"integrity": "sha512-V2xBGzBK+s2Iy7Re5JOcOBtAvaZtJ9t7R1fFOP51T6ynfSvJqaRtG4DjBu7i9inhXkCzrt7eGcX3vMqLCqXV8g==",
"requires": {
"@ant-design/colors": "^6.0.0",
"@ant-design/icons": "^4.6.3",
"@ant-design/icons": "^4.7.0",
"@ant-design/react-slick": "~0.28.1",
"@babel/runtime": "^7.12.5",
"@ctrl/tinycolor": "^3.4.0",
"array-tree-filter": "^2.1.0",
"classnames": "^2.2.6",
"copy-to-clipboard": "^3.2.0",
"lodash": "^4.17.21",
"moment": "^2.25.3",
"rc-cascader": "~1.4.0",
"rc-cascader": "~2.1.0",
"rc-checkbox": "~2.3.0",
"rc-collapse": "~3.1.0",
"rc-dialog": "~8.6.0",
"rc-drawer": "~4.3.0",
"rc-drawer": "~4.4.2",
"rc-dropdown": "~3.2.0",
"rc-field-form": "~1.20.0",
"rc-field-form": "~1.21.0",
"rc-image": "~5.2.5",
"rc-input-number": "~7.1.0",
"rc-input-number": "~7.3.0",
"rc-mentions": "~1.6.1",
"rc-menu": "~9.0.12",
"rc-motion": "^2.4.0",
"rc-motion": "^2.4.4",
"rc-notification": "~4.5.7",
"rc-pagination": "~3.1.9",
"rc-picker": "~2.5.10",
"rc-picker": "~2.5.17",
"rc-progress": "~3.1.0",
"rc-rate": "~2.9.0",
"rc-resize-observer": "^1.0.0",
"rc-select": "~12.1.6",
"rc-slider": "~9.7.1",
"rc-select": "~13.1.0-alpha.0",
"rc-slider": "~9.7.4",
"rc-steps": "~4.1.0",
"rc-switch": "~3.2.0",
"rc-table": "~7.15.1",
"rc-table": "~7.19.0",
"rc-tabs": "~11.10.0",
"rc-textarea": "~0.3.0",
"rc-tooltip": "~5.1.1",
"rc-tree": "~4.2.1",
"rc-tree-select": "~4.3.0",
"rc-tree": "~5.2.0",
"rc-tree-select": "~4.6.0",
"rc-trigger": "^5.2.10",
"rc-upload": "~4.3.0",
"rc-util": "^5.13.1",
"rc-util": "^5.14.0",
"scroll-into-view-if-needed": "^2.2.25"
},
"dependencies": {
@ -6735,12 +6769,13 @@
}
},
"rc-cascader": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-1.4.3.tgz",
"integrity": "sha512-Q4l9Mv8aaISJ+giVnM9IaXxDeMqHUGLvi4F+LksS6pHlaKlN4awop/L+IMjIXpL+ug/ojaCyv/ixcVopJYYCVA==",
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-2.1.5.tgz",
"integrity": "sha512-FiGPfSxKmSft2CT2XSr6HeKihqcxM+1ozmH6FGXTDthVNNvV0ai82CA6l30iPmMmlflwDfSm/623qkekqNq4BQ==",
"requires": {
"@babel/runtime": "^7.12.5",
"array-tree-filter": "^2.1.0",
"rc-tree-select": "~4.6.0",
"rc-trigger": "^5.0.4",
"rc-util": "^5.0.1",
"warning": "^4.0.1"
@ -6779,9 +6814,9 @@
}
},
"rc-drawer": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-4.3.1.tgz",
"integrity": "sha512-GMfFy4maqxS9faYXEhQ+0cA1xtkddEQzraf6SAdzWbn444DrrLogwYPk1NXSpdXjLCLxgxOj9MYtyYG42JsfXg==",
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-4.4.3.tgz",
"integrity": "sha512-FYztwRs3uXnFOIf1hLvFxIQP9MiZJA+0w+Os8dfDh/90X7z/HqP/Yg+noLCIeHEbKln1Tqelv8ymCAN24zPcfQ==",
"requires": {
"@babel/runtime": "^7.10.1",
"classnames": "^2.2.6",
@ -6799,12 +6834,12 @@
}
},
"rc-field-form": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-1.20.1.tgz",
"integrity": "sha512-f64KEZop7zSlrG4ef/PLlH12SLn6iHDQ3sTG+RfKBM45hikwV1i8qMf53xoX12NvXXWg1VwchggX/FSso4bWaA==",
"version": "1.21.2",
"resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-1.21.2.tgz",
"integrity": "sha512-LR/bURt/Tf5g39mb0wtMtQuWn42d/7kEzpzlC5fNC7yaRVmLTtlPP4sBBlaViETM9uZQKLoaB0Pt9Mubhm9gow==",
"requires": {
"@babel/runtime": "^7.8.4",
"async-validator": "^3.0.3",
"async-validator": "^4.0.2",
"rc-util": "^5.8.0"
}
},
@ -6820,9 +6855,9 @@
}
},
"rc-input-number": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-7.1.4.tgz",
"integrity": "sha512-EG4iqkqyqzLRu/Dq+fw2od7nlgvXLEatE+J6uhi3HXE1qlM3C7L6a7o/hL9Ly9nimkES2IeQoj3Qda3I0izj3Q==",
"version": "7.3.4",
"resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-7.3.4.tgz",
"integrity": "sha512-W9uqSzuvJUnz8H8vsVY4kx+yK51SsAxNTwr8SNH4G3XqQNocLVmKIibKFRjocnYX1RDHMND9FFbgj2h7E7nvGA==",
"requires": {
"@babel/runtime": "^7.10.1",
"classnames": "^2.2.5",
@ -6945,9 +6980,9 @@
}
},
"rc-select": {
"version": "12.1.13",
"resolved": "https://registry.npmjs.org/rc-select/-/rc-select-12.1.13.tgz",
"integrity": "sha512-cPI+aesP6dgCAaey4t4upDbEukJe+XN0DK6oO/6flcCX5o28o7KNZD7JAiVtC/6fCwqwI/kSs7S/43dvHmBl+A==",
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/rc-select/-/rc-select-13.1.1.tgz",
"integrity": "sha512-Oy4L27x5QgGR8902pw0bJVjrTWFnKPKvdLHzJl5pjiA+jM1hpzDfLGg/bY2ntk5ElxxQKZUwbFKUeqfCQU7SrQ==",
"requires": {
"@babel/runtime": "^7.10.1",
"classnames": "2.x",
@ -6970,9 +7005,9 @@
}
},
"rc-virtual-list": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.4.1.tgz",
"integrity": "sha512-YexJy+Cx8qjnQdV8+0JBeM65VF2kvO9lnsfrIvHsL3lIH1adMZ85HqmePGUzKkKMZC+CRAJc2K4g2iJS1dOjPw==",
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.4.2.tgz",
"integrity": "sha512-OyVrrPvvFcHvV0ssz5EDZ+7Rf5qLat/+mmujjchNw5FfbJWNDwkpQ99EcVE6+FtNRmX9wFa1LGNpZLUTvp/4GQ==",
"requires": {
"classnames": "^2.2.6",
"rc-resize-observer": "^1.0.0",
@ -6982,15 +7017,27 @@
}
},
"rc-slider": {
"version": "9.7.2",
"resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-9.7.2.tgz",
"integrity": "sha512-mVaLRpDo6otasBs6yVnG02ykI3K6hIrLTNfT5eyaqduFv95UODI9PDS6fWuVVehVpdS4ENgOSwsTjrPVun+k9g==",
"version": "9.7.5",
"resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-9.7.5.tgz",
"integrity": "sha512-LV/MWcXFjco1epPbdw1JlLXlTgmWpB9/Y/P2yinf8Pg3wElHxA9uajN21lJiWtZjf5SCUekfSP6QMJfDo4t1hg==",
"requires": {
"@babel/runtime": "^7.10.1",
"classnames": "^2.2.5",
"rc-tooltip": "^5.0.1",
"rc-util": "^5.0.0",
"rc-util": "^5.16.1",
"shallowequal": "^1.1.0"
},
"dependencies": {
"rc-util": {
"version": "5.16.1",
"resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.16.1.tgz",
"integrity": "sha512-kSCyytvdb3aRxQacS/71ta6c+kBWvM1v8/2h9d/HaNWauc3qB8pLnF20PJ8NajkNN8gb+rR1l0eWO+D4Pz+LLQ==",
"requires": {
"@babel/runtime": "^7.12.5",
"react-is": "^16.12.0",
"shallowequal": "^1.1.0"
}
}
}
},
"rc-steps": {
@ -7014,14 +7061,14 @@
}
},
"rc-table": {
"version": "7.15.2",
"resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.15.2.tgz",
"integrity": "sha512-TAs7kCpIZwc2mtvD8CMrXSM6TqJDUsy0rUEV1YgRru33T8bjtAtc+9xW/KC1VWROJlHSpU0R0kXjFs9h/6+IzQ==",
"version": "7.19.2",
"resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.19.2.tgz",
"integrity": "sha512-NdpnoM50MK02H5/hGOsObfxCvGFUG5cHB9turE5BKJ81T5Ycbq193w5tLhnpILXe//Oanzr47MdMxkUnVGP+qg==",
"requires": {
"@babel/runtime": "^7.10.1",
"classnames": "^2.2.5",
"rc-resize-observer": "^1.0.0",
"rc-util": "^5.13.0",
"rc-util": "^5.14.0",
"shallowequal": "^1.1.0"
}
},
@ -7059,21 +7106,21 @@
}
},
"rc-tree": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-4.2.2.tgz",
"integrity": "sha512-V1hkJt092VrOVjNyfj5IYbZKRMHxWihZarvA5hPL/eqm7o2+0SNkeidFYm7LVVBrAKBpOpa0l8xt04uiqOd+6w==",
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.2.2.tgz",
"integrity": "sha512-ZQPGi5rGmipXvSUqeMbh0Rm0Cn2zFVWQFvS3sinH+lis5VNCChkFs2dAFpWZnb9/d/SZPeMfYG/x2XFq/q3UTA==",
"requires": {
"@babel/runtime": "^7.10.1",
"classnames": "2.x",
"rc-motion": "^2.0.1",
"rc-util": "^5.0.0",
"rc-virtual-list": "^3.0.1"
"rc-virtual-list": "^3.4.1"
},
"dependencies": {
"rc-virtual-list": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.4.1.tgz",
"integrity": "sha512-YexJy+Cx8qjnQdV8+0JBeM65VF2kvO9lnsfrIvHsL3lIH1adMZ85HqmePGUzKkKMZC+CRAJc2K4g2iJS1dOjPw==",
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.4.2.tgz",
"integrity": "sha512-OyVrrPvvFcHvV0ssz5EDZ+7Rf5qLat/+mmujjchNw5FfbJWNDwkpQ99EcVE6+FtNRmX9wFa1LGNpZLUTvp/4GQ==",
"requires": {
"classnames": "^2.2.6",
"rc-resize-observer": "^1.0.0",
@ -7083,15 +7130,15 @@
}
},
"rc-tree-select": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-4.3.3.tgz",
"integrity": "sha512-0tilOHLJA6p+TNg4kD559XnDX3PTEYuoSF7m7ryzFLAYvdEEPtjn0QZc5z6L0sMKBiBlj8a2kf0auw8XyHU3lA==",
"version": "4.6.3",
"resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-4.6.3.tgz",
"integrity": "sha512-VymfystOnW8EfoWaWehgB8zpYKgRZf4ILu9KHf7FJZVZ/1dnBEHDqg1bBi43/1BYLwYFKSKKSjkYyNYntWJM4A==",
"requires": {
"@babel/runtime": "^7.10.1",
"classnames": "2.x",
"rc-select": "^12.0.0",
"rc-tree": "^4.0.0",
"rc-util": "^5.0.5"
"rc-select": "~13.1.0-alpha.0",
"rc-tree": "~5.2.0",
"rc-util": "^5.7.0"
}
},
"rc-trigger": {
@ -7255,9 +7302,9 @@
"peer": true
},
"async-validator": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-3.5.2.tgz",
"integrity": "sha512-8eLCg00W9pIRZSB781UUX/H6Oskmm8xloZfr09lz5bikRpBVDlJ3hRVuxxP1SxcwsEYfJ4IU8Q19Y8/893r3rQ=="
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.0.7.tgz",
"integrity": "sha512-Pj2IR7u8hmUEDOwB++su6baaRi+QvsgajuFB9j95foM1N2gy5HM4z60hfusIO0fBPG5uLAEl6yCJr1jNSVugEQ=="
},
"atob": {
"version": "2.1.2",
@ -7909,6 +7956,8 @@
"cvat-canvas": {
"version": "file:../cvat-canvas",
"requires": {
"@types/polylabel": "^1.0.5",
"polylabel": "^1.1.0",
"svg.draggable.js": "2.2.2",
"svg.draw.js": "^2.0.4",
"svg.js": "2.7.1",
@ -7931,7 +7980,7 @@
"browser-or-node": "^1.2.1",
"coveralls": "^3.0.5",
"cvat-data": "../cvat-data",
"detect-browser": "^5.2.0",
"detect-browser": "^5.2.1",
"error-stack-parser": "^2.0.2",
"form-data": "^2.5.0",
"jest": "^26.6.3",
@ -7943,7 +7992,7 @@
"platform": "^1.3.5",
"quickhull": "^1.0.3",
"store": "^2.0.12",
"worker-loader": "^2.0.0"
"tus-js-client": "^2.3.0"
}
},
"cyclist": {
@ -9850,6 +9899,16 @@
"jsonp": "^0.2.1"
}
},
"react-sortable-hoc": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-sortable-hoc/-/react-sortable-hoc-2.0.0.tgz",
"integrity": "sha512-JZUw7hBsAHXK7PTyErJyI7SopSBFRcFHDjWW5SWjcugY0i6iH7f+eJkY8cJmGMlZ1C9xz1J3Vjz0plFpavVeRg==",
"requires": {
"@babel/runtime": "^7.2.0",
"invariant": "^2.2.4",
"prop-types": "^15.5.7"
}
},
"reactcss": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",

@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.25.0",
"version": "1.36.0",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
@ -19,7 +19,6 @@
],
"author": "Intel",
"license": "MIT",
"devDependencies": {},
"dependencies": {
"@ant-design/icons": "^4.6.3",
"@types/lodash": "^4.14.172",
@ -34,7 +33,7 @@
"@types/react-share": "^3.0.3",
"@types/redux-logger": "^3.0.9",
"@types/resize-observer-browser": "^0.1.6",
"antd": "^4.16.13",
"antd": "^4.17.0",
"copy-to-clipboard": "^3.3.1",
"cvat-canvas": "file:../cvat-canvas",
"cvat-canvas3d": "file:../cvat-canvas3d",
@ -57,6 +56,7 @@
"react-router": "^5.1.0",
"react-router-dom": "^5.1.0",
"react-share": "^4.4.0",
"react-sortable-hoc": "^2.0.0",
"redux": "^4.1.1",
"redux-devtools-extension": "^2.13.9",
"redux-logger": "^3.0.6",

@ -1,7 +1,17 @@
server {
root /usr/share/nginx/html;
# Any route that doesn't have a file extension (e.g. /devices)
location / {
# Any route that doesn't exist on the server (e.g. /devices)
try_files $uri $uri/ /index.html;
add_header Cache-Control: "no-cache, no-store, must-revalidate";
add_header Pragma: "no-cache";
add_header Expires: 0;
}
location /assets {
expires 1y;
add_header Cache-Control "public";
access_log off;
}
}

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -6,10 +6,12 @@ import {
ActionCreator, AnyAction, Dispatch, Store,
} from 'redux';
import { ThunkAction } from 'utils/redux';
import isAbleToChangeFrame from 'utils/is-able-to-change-frame';
import { RectDrawingMethod, CuboidDrawingMethod, Canvas } from 'cvat-canvas-wrapper';
import getCore from 'cvat-core-wrapper';
import logger, { LogType } from 'cvat-logger';
import { getCVATStore } from 'cvat-store';
import {
ActiveControl,
CombinedState,
@ -24,6 +26,8 @@ import {
Task,
Workspace,
} from 'reducers/interfaces';
import { updateJobAsync } from './tasks-actions';
import { switchToolsBlockerState } from './settings-actions';
interface AnnotationsParameters {
filters: string[];
@ -183,8 +187,6 @@ export enum AnnotationActionTypes {
SAVE_LOGS_FAILED = 'SAVE_LOGS_FAILED',
INTERACT_WITH_CANVAS = 'INTERACT_WITH_CANVAS',
GET_DATA_FAILED = 'GET_DATA_FAILED',
SWITCH_REQUEST_REVIEW_DIALOG = 'SWITCH_REQUEST_REVIEW_DIALOG',
SWITCH_SUBMIT_REVIEW_DIALOG = 'SWITCH_SUBMIT_REVIEW_DIALOG',
SET_FORCE_EXIT_ANNOTATION_PAGE_FLAG = 'SET_FORCE_EXIT_ANNOTATION_PAGE_FLAG',
UPDATE_PREDICTOR_STATE = 'UPDATE_PREDICTOR_STATE',
GET_PREDICTIONS = 'GET_PREDICTIONS',
@ -343,7 +345,7 @@ export function uploadJobAnnotationsAsync(job: any, loader: any, file: File): Th
const state: CombinedState = getStore().getState();
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
if (state.tasks.activities.loads[job.task.id]) {
if (state.tasks.activities.loads[job.taskId]) {
throw Error('Annotations is being uploaded for the task');
}
if (state.annotation.activities.loads[job.id]) {
@ -639,7 +641,7 @@ export function getPredictionsAsync(): ThunkAction {
annotations = annotations.map(
(data: any): any => new cvat.classes.ObjectState({
shapeType: data.type,
label: job.task.labels.filter((label: any): boolean => label.id === data.label)[0],
label: job.labels.filter((label: any): boolean => label.id === data.label)[0],
points: data.points,
objectType: ObjectType.SHAPE,
frame,
@ -692,8 +694,8 @@ export function changeFrameAsync(
frameStep?: number,
forceUpdate?: boolean,
): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const state: CombinedState = getStore().getState();
return async (dispatch: ActionCreator<Dispatch>, getState: () => CombinedState): Promise<void> => {
const state: CombinedState = getState();
const { instance: job } = state.annotation.job;
const { filters, frame, showAllInterpolationTracks } = receiveAnnotationsParameters();
@ -702,37 +704,52 @@ export function changeFrameAsync(
throw Error(`Required frame ${toFrame} is out of the current job`);
}
if (toFrame === frame && !forceUpdate) {
dispatch({
const abortAction = (): AnyAction => {
const currentState = getState();
return ({
type: AnnotationActionTypes.CHANGE_FRAME_SUCCESS,
payload: {
number: state.annotation.player.frame.number,
data: state.annotation.player.frame.data,
filename: state.annotation.player.frame.filename,
hasRelatedContext: state.annotation.player.frame.hasRelatedContext,
delay: state.annotation.player.frame.delay,
changeTime: state.annotation.player.frame.changeTime,
states: state.annotation.annotations.states,
minZ: state.annotation.annotations.zLayer.min,
maxZ: state.annotation.annotations.zLayer.max,
curZ: state.annotation.annotations.zLayer.cur,
number: currentState.annotation.player.frame.number,
data: currentState.annotation.player.frame.data,
filename: currentState.annotation.player.frame.filename,
hasRelatedContext: currentState.annotation.player.frame.hasRelatedContext,
delay: currentState.annotation.player.frame.delay,
changeTime: currentState.annotation.player.frame.changeTime,
states: currentState.annotation.annotations.states,
minZ: currentState.annotation.annotations.zLayer.min,
maxZ: currentState.annotation.annotations.zLayer.max,
curZ: currentState.annotation.annotations.zLayer.cur,
},
});
};
return;
}
// Start async requests
dispatch({
type: AnnotationActionTypes.CHANGE_FRAME,
payload: {},
});
if (toFrame === frame && !forceUpdate) {
dispatch(abortAction());
return;
}
const data = await job.frames.get(toFrame, fillBuffer, frameStep);
const states = await job.annotations.get(toFrame, showAllInterpolationTracks, filters);
if (!isAbleToChangeFrame()) {
// while doing async actions above, canvas can become used by a user in another way
// so, we need an additional check and if it is used, we do not update state
dispatch(abortAction());
return;
}
// commit the latest job frame to local storage
localStorage.setItem(`Job_${job.id}_frame`, `${toFrame}`);
await job.logger.log(LogType.changeFrame, {
from: frame,
to: toFrame,
});
const data = await job.frames.get(toFrame, fillBuffer, frameStep);
const states = await job.annotations.get(toFrame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states);
const currentTime = new Date().getTime();
let frameSpeed;
@ -950,7 +967,7 @@ export function closeJob(): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const { jobInstance } = receiveAnnotationsParameters();
if (jobInstance) {
await jobInstance.task.close();
await jobInstance.close();
}
dispatch({
@ -960,9 +977,9 @@ export function closeJob(): ThunkAction {
}
export function getJobAsync(tid: number, jid: number, initialFrame: number, initialFilters: object[]): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
return async (dispatch: ActionCreator<Dispatch>, getState): Promise<void> => {
try {
const state: CombinedState = getStore().getState();
const state = getState();
const filters = initialFilters;
const {
settings: {
@ -986,20 +1003,18 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init
true,
);
// Check state if the task is already there
let task = state.tasks.current
// Check if the task was already downloaded to the state
let job: any | null = null;
const [task] = state.tasks.current
.filter((_task: Task) => _task.instance.id === tid)
.map((_task: Task) => _task.instance)[0];
// If there aren't the task, get it from the server
if (!task) {
[task] = await cvat.tasks.get({ id: tid });
}
// Finally get the job from the task
const job = task.jobs.filter((_job: any) => _job.id === jid)[0];
if (!job) {
throw new Error(`Task ${tid} doesn't contain the job ${jid}`);
.map((_task: Task) => _task.instance);
if (task) {
[job] = task.jobs.filter((_job: any) => _job.id === jid);
if (!job) {
throw new Error(`Task ${tid} doesn't contain the job ${jid}`);
}
} else {
[job] = await cvat.jobs.get({ jobID: jid });
}
const frameNumber = Math.max(Math.min(job.stopFrame, initialFrame), job.startFrame);
@ -1018,7 +1033,6 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init
}
const states = await job.annotations.get(frameNumber, showAllInterpolationTracks, filters);
const issues = await job.issues();
const reviews = await job.reviews();
const [minZ, maxZ] = computeZRange(states);
const colors = [...cvat.enums.colors];
@ -1031,7 +1045,6 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init
openTime,
job,
issues,
reviews,
states,
frameNumber,
frameFilename: frameData.filename,
@ -1044,14 +1057,14 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init
},
});
if (job.task.dimension === DimensionType.DIM_3D) {
if (job.dimension === DimensionType.DIM_3D) {
const workspace = Workspace.STANDARD3D;
dispatch(changeWorkspace(workspace));
}
const updatePredictorStatus = async (): Promise<void> => {
// get current job
const currentState: CombinedState = getStore().getState();
const currentState: CombinedState = getState();
const { openTime: currentOpenTime, instance: currentJob } = currentState.annotation.job;
if (currentJob === null || currentJob.id !== job.id || currentOpenTime !== openTime) {
// the job was closed, changed or reopened
@ -1074,7 +1087,7 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init
}
};
if (state.plugins.list.PREDICT && job.task.projectId !== null) {
if (state.plugins.list.PREDICT && job.projectId !== null) {
updatePredictorStatus();
}
@ -1120,6 +1133,11 @@ export function saveAnnotationsAsync(sessionInstance: any, afterSave?: () => voi
afterSave();
}
if (sessionInstance instanceof cvat.classes.Job && sessionInstance.state === cvat.enums.JobState.NEW) {
sessionInstance.state = cvat.enums.JobState.IN_PROGRESS;
dispatch(updateJobAsync(sessionInstance));
}
dispatch({
type: AnnotationActionTypes.SAVE_ANNOTATIONS_SUCCESS,
payload: {
@ -1488,12 +1506,13 @@ export function repeatDrawShapeAsync(): ThunkAction {
let activeControl = ActiveControl.CURSOR;
if (activeInteractor && canvasInstance instanceof Canvas) {
if (activeInteractor.type === 'tracker') {
if (activeInteractor.type.includes('tracker')) {
canvasInstance.interact({
enabled: true,
shapeType: 'rectangle',
});
dispatch(interactWithCanvas(activeInteractor, activeLabelID));
dispatch(switchToolsBlockerState({ buttonVisible: false }));
} else {
canvasInstance.interact({
enabled: true,
@ -1515,7 +1534,10 @@ export function repeatDrawShapeAsync(): ThunkAction {
activeControl = ActiveControl.DRAW_POLYLINE;
} else if (activeShapeType === ShapeType.CUBOID) {
activeControl = ActiveControl.DRAW_CUBOID;
} else if (activeShapeType === ShapeType.ELLIPSE) {
activeControl = ActiveControl.DRAW_ELLIPSE;
}
dispatch({
type: AnnotationActionTypes.REPEAT_DRAW_SHAPE,
payload: {
@ -1533,14 +1555,14 @@ export function repeatDrawShapeAsync(): ThunkAction {
frame: frameNumber,
});
dispatch(createAnnotationsAsync(jobInstance, frameNumber, [objectState]));
} else {
} else if (canvasInstance) {
canvasInstance.draw({
enabled: true,
rectDrawingMethod: activeRectDrawingMethod,
cuboidDrawingMethod: activeCuboidDrawingMethod,
numberOfPoints: activeNumOfPoints,
shapeType: activeShapeType,
crosshair: [ShapeType.RECTANGLE, ShapeType.CUBOID].includes(activeShapeType),
crosshair: [ShapeType.RECTANGLE, ShapeType.CUBOID, ShapeType.ELLIPSE].includes(activeShapeType),
});
}
};
@ -1582,31 +1604,13 @@ export function redrawShapeAsync(): ThunkAction {
enabled: true,
redraw: activatedStateID,
shapeType: state.shapeType,
crosshair: [ShapeType.RECTANGLE, ShapeType.CUBOID].includes(state.shapeType),
crosshair: [ShapeType.RECTANGLE, ShapeType.CUBOID, ShapeType.ELLIPSE].includes(state.shapeType),
});
}
}
};
}
export function switchRequestReviewDialog(visible: boolean): AnyAction {
return {
type: AnnotationActionTypes.SWITCH_REQUEST_REVIEW_DIALOG,
payload: {
visible,
},
};
}
export function switchSubmitReviewDialog(visible: boolean): AnyAction {
return {
type: AnnotationActionTypes.SWITCH_SUBMIT_REVIEW_DIALOG,
payload: {
visible,
},
};
}
export function setForceExitAnnotationFlag(forceExit: boolean): AnyAction {
return {
type: AnnotationActionTypes.SET_FORCE_EXIT_ANNOTATION_PAGE_FLAG,
@ -1645,7 +1649,7 @@ export function getContextImageAsync(): ThunkAction {
payload: {},
});
const contextImageData = await job.frames.contextImage(job.task.id, frameNumber);
const contextImageData = await job.frames.contextImage(frameNumber);
dispatch({
type: AnnotationActionTypes.GET_CONTEXT_IMAGE_SUCCESS,
payload: { contextImageData },

@ -106,7 +106,6 @@ export const loginAsync = (username: string, password: string): ThunkAction => a
try {
await cvat.server.login(username, password);
const users = await cvat.users.get({ self: true });
dispatch(authActions.loginSuccess(users[0]));
} catch (error) {
dispatch(authActions.loginFailed(error));
@ -117,6 +116,7 @@ export const logoutAsync = (): ThunkAction => async (dispatch) => {
dispatch(authActions.logout());
try {
await cvat.organizations.deactivate();
await cvat.server.logout();
dispatch(authActions.logoutSuccess());
} catch (error) {

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -103,6 +103,13 @@ export type CloudStorageActions = ActionUnion<typeof cloudStoragesActions>;
export function getCloudStoragesAsync(query: Partial<CloudStoragesQuery>): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
function camelToSnake(str: string): string {
return (
str[0].toLowerCase() + str.slice(1, str.length)
.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
);
}
dispatch(cloudStoragesActions.getCloudStorages());
dispatch(cloudStoragesActions.updateCloudStoragesGettingQuery(query));
@ -113,6 +120,23 @@ export function getCloudStoragesAsync(query: Partial<CloudStoragesQuery>): Thunk
}
}
// Temporary hack to do not change UI currently for cloud storages
// Will be redesigned in a different PR
const filter = {
and: ['displayName', 'resource', 'description', 'owner', 'providerType', 'credentialsType'].reduce<object[]>((acc, filterField) => {
if (filterField in filteredQuery) {
acc.push({ '==': [{ var: camelToSnake(filterField) }, filteredQuery[filterField]] });
delete filteredQuery[filterField];
}
return acc;
}, []),
};
if (filter.and.length) {
filteredQuery.filter = JSON.stringify(filter);
}
let result = null;
try {
result = await cvat.cloudStorages.get(filteredQuery);

@ -0,0 +1,59 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { createAction, ActionUnion, ThunkAction } from 'utils/redux';
import { CombinedState } from 'reducers/interfaces';
import { getProjectsAsync } from './projects-actions';
export enum ImportActionTypes {
OPEN_IMPORT_MODAL = 'OPEN_IMPORT_MODAL',
CLOSE_IMPORT_MODAL = 'CLOSE_IMPORT_MODAL',
IMPORT_DATASET = 'IMPORT_DATASET',
IMPORT_DATASET_SUCCESS = 'IMPORT_DATASET_SUCCESS',
IMPORT_DATASET_FAILED = 'IMPORT_DATASET_FAILED',
IMPORT_DATASET_UPDATE_STATUS = 'IMPORT_DATASET_UPDATE_STATUS',
}
export const importActions = {
openImportModal: (instance: any) => createAction(ImportActionTypes.OPEN_IMPORT_MODAL, { instance }),
closeImportModal: () => createAction(ImportActionTypes.CLOSE_IMPORT_MODAL),
importDataset: (projectId: number) => (
createAction(ImportActionTypes.IMPORT_DATASET, { id: projectId })
),
importDatasetSuccess: () => (
createAction(ImportActionTypes.IMPORT_DATASET_SUCCESS)
),
importDatasetFailed: (instance: any, error: any) => (
createAction(ImportActionTypes.IMPORT_DATASET_FAILED, {
instance,
error,
})
),
importDatasetUpdateStatus: (progress: number, status: string) => (
createAction(ImportActionTypes.IMPORT_DATASET_UPDATE_STATUS, { progress, status })
),
};
export const importDatasetAsync = (instance: any, format: string, file: File): ThunkAction => (
async (dispatch, getState) => {
try {
const state: CombinedState = getState();
if (state.import.importingId !== null) {
throw Error('Only one importing of dataset allowed at the same time');
}
dispatch(importActions.importDataset(instance.id));
await instance.annotations.importDataset(format, file, (message: string, progress: number) => (
dispatch(importActions.importDatasetUpdateStatus(progress * 100, message))
));
} catch (error) {
dispatch(importActions.importDatasetFailed(instance, error));
return;
}
dispatch(importActions.importDatasetSuccess());
dispatch(getProjectsAsync({ id: instance.id }));
}
);
export type ImportActions = ActionUnion<typeof importActions>;

@ -0,0 +1,47 @@
// Copyright (C) 2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import getCore from 'cvat-core-wrapper';
import { JobsQuery } from 'reducers/interfaces';
const cvat = getCore();
export enum JobsActionTypes {
GET_JOBS = 'GET_JOBS',
GET_JOBS_SUCCESS = 'GET_JOBS_SUCCESS',
GET_JOBS_FAILED = 'GET_JOBS_FAILED',
}
interface JobsList extends Array<any> {
count: number;
}
const jobsActions = {
getJobs: (query: Partial<JobsQuery>) => createAction(JobsActionTypes.GET_JOBS, { query }),
getJobsSuccess: (jobs: JobsList, previews: string[]) => (
createAction(JobsActionTypes.GET_JOBS_SUCCESS, { jobs, previews })
),
getJobsFailed: (error: any) => createAction(JobsActionTypes.GET_JOBS_FAILED, { error }),
};
export type JobsActions = ActionUnion<typeof jobsActions>;
export const getJobsAsync = (query: JobsQuery): ThunkAction => async (dispatch) => {
try {
// Remove all keys with null values from the query
const filteredQuery: Partial<JobsQuery> = { ...query };
if (filteredQuery.page === null) delete filteredQuery.page;
if (filteredQuery.filter === null) delete filteredQuery.filter;
if (filteredQuery.sort === null) delete filteredQuery.sort;
if (filteredQuery.search === null) delete filteredQuery.search;
dispatch(jobsActions.getJobs(filteredQuery));
const jobs = await cvat.jobs.get(filteredQuery);
const previewPromises = jobs.map((job: any) => (job as any).frames.preview().catch(() => ''));
dispatch(jobsActions.getJobsSuccess(jobs, await Promise.all(previewPromises)));
} catch (error) {
dispatch(jobsActions.getJobsFailed(error));
}
};

@ -146,23 +146,23 @@ export function getInferenceStatusAsync(): ThunkAction {
};
}
export function startInferenceAsync(taskInstance: any, model: Model, body: object): ThunkAction {
export function startInferenceAsync(taskId: number, model: Model, body: object): ThunkAction {
return async (dispatch): Promise<void> => {
try {
const requestID: string = await core.lambda.run(taskInstance, model, body);
const requestID: string = await core.lambda.run(taskId, model, body);
const dispatchCallback = (action: ModelsActions): void => {
dispatch(action);
};
listen(
{
taskID: taskInstance.id,
taskID: taskId,
requestID,
},
dispatchCallback,
);
} catch (error) {
dispatch(modelsActions.startInferenceFailed(taskInstance.id, error));
dispatch(modelsActions.startInferenceFailed(taskId, error));
}
};
}

@ -0,0 +1,266 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { Store } from 'antd/lib/form/interface';
import { User } from 'components/task-page/user-selector';
import getCore from 'cvat-core-wrapper';
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
const core = getCore();
export enum OrganizationActionsTypes {
GET_ORGANIZATIONS = 'GET_ORGANIZATIONS',
GET_ORGANIZATIONS_SUCCESS = 'GET_ORGANIZATIONS_SUCCESS',
GET_ORGANIZATIONS_FAILED = 'GET_ORGANIZATIONS_FAILED',
ACTIVATE_ORGANIZATION_SUCCESS = 'ACTIVATE_ORGANIZATION_SUCCESS',
ACTIVATE_ORGANIZATION_FAILED = 'ACTIVATE_ORGANIZATION_FAILED',
CREATE_ORGANIZATION = 'CREATE_ORGANIZATION',
CREATE_ORGANIZATION_SUCCESS = 'CREATE_ORGANIZATION_SUCCESS',
CREATE_ORGANIZATION_FAILED = 'CREATE_ORGANIZATION_FAILED',
UPDATE_ORGANIZATION = 'UPDATE_ORGANIZATION',
UPDATE_ORGANIZATION_SUCCESS = 'UPDATE_ORGANIZATION_SUCCESS',
UPDATE_ORGANIZATION_FAILED = 'UPDATE_ORGANIZATION_FAILED',
REMOVE_ORGANIZATION = 'REMOVE_ORGANIZATION',
REMOVE_ORGANIZATION_SUCCESS = 'REMOVE_ORGANIZATION_SUCCESS',
REMOVE_ORGANIZATION_FAILED = 'REMOVE_ORGANIZATION_FAILED',
INVITE_ORGANIZATION_MEMBERS = 'INVITE_ORGANIZATION_MEMBERS',
INVITE_ORGANIZATION_MEMBERS_FAILED = 'INVITE_ORGANIZATION_MEMBERS_FAILED',
INVITE_ORGANIZATION_MEMBERS_DONE = 'INVITE_ORGANIZATION_MEMBERS_DONE',
INVITE_ORGANIZATION_MEMBER_SUCCESS = 'INVITE_ORGANIZATION_MEMBER_SUCCESS',
INVITE_ORGANIZATION_MEMBER_FAILED = 'INVITE_ORGANIZATION_MEMBER_FAILED',
LEAVE_ORGANIZATION = 'LEAVE_ORGANIZATION',
LEAVE_ORGANIZATION_SUCCESS = 'LEAVE_ORGANIZATION_SUCCESS',
LEAVE_ORGANIZATION_FAILED = 'LEAVE_ORGANIZATION_FAILED',
REMOVE_ORGANIZATION_MEMBER = 'REMOVE_ORGANIZATION_MEMBERS',
REMOVE_ORGANIZATION_MEMBER_SUCCESS = 'REMOVE_ORGANIZATION_MEMBER_SUCCESS',
REMOVE_ORGANIZATION_MEMBER_FAILED = 'REMOVE_ORGANIZATION_MEMBER_FAILED',
UPDATE_ORGANIZATION_MEMBER = 'UPDATE_ORGANIZATION_MEMBER',
UPDATE_ORGANIZATION_MEMBER_SUCCESS = 'UPDATE_ORGANIZATION_MEMBER_SUCCESS',
UPDATE_ORGANIZATION_MEMBER_FAILED = 'UPDATE_ORGANIZATION_MEMBER_FAILED',
}
const organizationActions = {
getOrganizations: () => createAction(OrganizationActionsTypes.GET_ORGANIZATIONS),
getOrganizationsSuccess: (list: any[]) => createAction(
OrganizationActionsTypes.GET_ORGANIZATIONS_SUCCESS, { list },
),
getOrganizationsFailed: (error: any) => createAction(OrganizationActionsTypes.GET_ORGANIZATIONS_FAILED, { error }),
createOrganization: () => createAction(OrganizationActionsTypes.CREATE_ORGANIZATION),
createOrganizationSuccess: (organization: any) => createAction(
OrganizationActionsTypes.CREATE_ORGANIZATION_SUCCESS, { organization },
),
createOrganizationFailed: (slug: string, error: any) => createAction(
OrganizationActionsTypes.CREATE_ORGANIZATION_FAILED, { slug, error },
),
updateOrganization: () => createAction(OrganizationActionsTypes.UPDATE_ORGANIZATION),
updateOrganizationSuccess: (organization: any) => createAction(
OrganizationActionsTypes.UPDATE_ORGANIZATION_SUCCESS, { organization },
),
updateOrganizationFailed: (slug: string, error: any) => createAction(
OrganizationActionsTypes.UPDATE_ORGANIZATION_FAILED, { slug, error },
),
activateOrganizationSuccess: (organization: any | null) => createAction(
OrganizationActionsTypes.ACTIVATE_ORGANIZATION_SUCCESS, { organization },
),
activateOrganizationFailed: (error: any, slug: string | null) => createAction(
OrganizationActionsTypes.ACTIVATE_ORGANIZATION_FAILED, { slug, error },
),
removeOrganization: () => createAction(OrganizationActionsTypes.REMOVE_ORGANIZATION),
removeOrganizationSuccess: (slug: string) => createAction(
OrganizationActionsTypes.REMOVE_ORGANIZATION_SUCCESS, { slug },
),
removeOrganizationFailed: (error: any, slug: string) => createAction(
OrganizationActionsTypes.REMOVE_ORGANIZATION_FAILED, { error, slug },
),
inviteOrganizationMembers: () => createAction(OrganizationActionsTypes.INVITE_ORGANIZATION_MEMBERS),
inviteOrganizationMembersFailed: (error: any) => createAction(
OrganizationActionsTypes.INVITE_ORGANIZATION_MEMBERS_FAILED, { error },
),
inviteOrganizationMembersDone: () => createAction(OrganizationActionsTypes.INVITE_ORGANIZATION_MEMBERS_DONE),
inviteOrganizationMemberSuccess: (email: string) => createAction(
OrganizationActionsTypes.INVITE_ORGANIZATION_MEMBER_SUCCESS, { email },
),
inviteOrganizationMemberFailed: (email: string, error: any) => createAction(
OrganizationActionsTypes.INVITE_ORGANIZATION_MEMBER_FAILED, { email, error },
),
leaveOrganization: () => createAction(OrganizationActionsTypes.LEAVE_ORGANIZATION),
leaveOrganizationSuccess: () => createAction(OrganizationActionsTypes.LEAVE_ORGANIZATION_SUCCESS),
leaveOrganizationFailed: (error: any) => createAction(
OrganizationActionsTypes.LEAVE_ORGANIZATION_FAILED, { error },
),
removeOrganizationMember: () => createAction(OrganizationActionsTypes.REMOVE_ORGANIZATION_MEMBER),
removeOrganizationMemberSuccess: () => createAction(OrganizationActionsTypes.REMOVE_ORGANIZATION_MEMBER_SUCCESS),
removeOrganizationMemberFailed: (username: string, error: any) => createAction(
OrganizationActionsTypes.REMOVE_ORGANIZATION_MEMBER_FAILED, { username, error },
),
updateOrganizationMember: () => createAction(OrganizationActionsTypes.UPDATE_ORGANIZATION_MEMBER),
updateOrganizationMemberSuccess: () => createAction(OrganizationActionsTypes.UPDATE_ORGANIZATION_MEMBER_SUCCESS),
updateOrganizationMemberFailed: (username: string, role: string, error: any) => createAction(
OrganizationActionsTypes.UPDATE_ORGANIZATION_MEMBER_FAILED, { username, role, error },
),
};
export function getOrganizationsAsync(): ThunkAction {
return async function (dispatch) {
dispatch(organizationActions.getOrganizations());
try {
const organizations = await core.organizations.get();
let currentOrganization = null;
try {
// this action is dispatched after user is authentificated
// need to configure organization at cvat-core immediately to get relevant data
const curSlug = localStorage.getItem('currentOrganization');
if (curSlug) {
currentOrganization =
organizations.find((organization: any) => organization.slug === curSlug) || null;
if (currentOrganization) {
await core.organizations.activate(currentOrganization);
} else {
// not valid anymore (for example when organization
// does not exist anymore, or the user has been kicked from it)
localStorage.removeItem('currentOrganization');
}
}
dispatch(organizationActions.activateOrganizationSuccess(currentOrganization));
} catch (error) {
dispatch(
organizationActions.activateOrganizationFailed(error, localStorage.getItem('currentOrganization')),
);
} finally {
dispatch(organizationActions.getOrganizationsSuccess(organizations));
}
} catch (error) {
dispatch(organizationActions.getOrganizationsFailed(error));
}
};
}
export function createOrganizationAsync(
organizationData: Store,
onCreateSuccess?: (createdSlug: string) => void,
): ThunkAction {
return async function (dispatch) {
const { slug } = organizationData;
const organization = new core.classes.Organization(organizationData);
dispatch(organizationActions.createOrganization());
try {
const createdOrganization = await organization.save();
dispatch(organizationActions.createOrganizationSuccess(createdOrganization));
if (onCreateSuccess) onCreateSuccess(createdOrganization.slug);
} catch (error) {
dispatch(organizationActions.createOrganizationFailed(slug, error));
}
};
}
export function updateOrganizationAsync(organization: any): ThunkAction {
return async function (dispatch) {
dispatch(organizationActions.updateOrganization());
try {
const updatedOrganization = await organization.save();
dispatch(organizationActions.updateOrganizationSuccess(updatedOrganization));
} catch (error) {
dispatch(organizationActions.updateOrganizationFailed(organization.slug, error));
}
};
}
export function removeOrganizationAsync(organization: any): ThunkAction {
return async function (dispatch) {
try {
await organization.remove();
localStorage.removeItem('currentOrganization');
dispatch(organizationActions.removeOrganizationSuccess(organization.slug));
} catch (error) {
dispatch(organizationActions.removeOrganizationFailed(error, organization.slug));
}
};
}
export function inviteOrganizationMembersAsync(
organization: any,
members: { email: string; role: string }[],
onFinish: () => void,
): ThunkAction {
return async function (dispatch) {
dispatch(organizationActions.inviteOrganizationMembers());
try {
for (let i = 0; i < members.length; i++) {
const { email, role } = members[i];
organization
.invite(email, role)
.then(() => {
dispatch(organizationActions.inviteOrganizationMemberSuccess(email));
})
.catch((error: any) => {
dispatch(organizationActions.inviteOrganizationMemberFailed(email, error));
})
.finally(() => {
if (i === members.length - 1) {
dispatch(organizationActions.inviteOrganizationMembersDone());
onFinish();
}
});
}
} catch (error) {
dispatch(organizationActions.inviteOrganizationMembersFailed(error));
}
};
}
export function leaveOrganizationAsync(organization: any): ThunkAction {
return async function (dispatch, getState) {
const { user } = getState().auth;
dispatch(organizationActions.leaveOrganization());
try {
await organization.leave(user);
dispatch(organizationActions.leaveOrganizationSuccess());
localStorage.removeItem('currentOrganization');
} catch (error) {
dispatch(organizationActions.leaveOrganizationFailed(error));
}
};
}
export function removeOrganizationMemberAsync(
organization: any,
{ user, id }: { user: User; id: number },
onFinish: () => void,
): ThunkAction {
return async function (dispatch) {
dispatch(organizationActions.removeOrganizationMember());
try {
await organization.deleteMembership(id);
dispatch(organizationActions.removeOrganizationMemberSuccess());
onFinish();
} catch (error) {
dispatch(organizationActions.removeOrganizationMemberFailed(user.username, error));
}
};
}
export function updateOrganizationMemberAsync(
organization: any,
{ user, id }: { user: User; id: number },
role: string,
onFinish: () => void,
): ThunkAction {
return async function (dispatch) {
dispatch(organizationActions.updateOrganizationMember());
try {
await organization.updateMembership(id, role);
dispatch(organizationActions.updateOrganizationMemberSuccess());
onFinish();
} catch (error) {
dispatch(organizationActions.updateOrganizationMemberFailed(user.username, role, error));
}
};
}
export type OrganizationActions = ActionUnion<typeof organizationActions>;

@ -1,12 +1,13 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { Dispatch, ActionCreator } from 'redux';
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import { ProjectsQuery } from 'reducers/interfaces';
import { getTasksSuccess, updateTaskSuccess } from 'actions/tasks-actions';
import { ProjectsQuery, TasksQuery, CombinedState } from 'reducers/interfaces';
import { getTasksAsync } from 'actions/tasks-actions';
import { getCVATStore } from 'cvat-store';
import getCore from 'cvat-core-wrapper';
const cvat = getCore();
@ -25,6 +26,12 @@ export enum ProjectsActionTypes {
DELETE_PROJECT = 'DELETE_PROJECT',
DELETE_PROJECT_SUCCESS = 'DELETE_PROJECT_SUCCESS',
DELETE_PROJECT_FAILED = 'DELETE_PROJECT_FAILED',
BACKUP_PROJECT = 'BACKUP_PROJECT',
BACKUP_PROJECT_SUCCESS = 'BACKUP_PROJECT_SUCCESS',
BACKUP_PROJECT_FAILED = 'BACKUP_PROJECT_FAILED',
RESTORE_PROJECT = 'IMPORT_PROJECT',
RESTORE_PROJECT_SUCCESS = 'IMPORT_PROJECT_SUCCESS',
RESTORE_PROJECT_FAILED = 'IMPORT_PROJECT_FAILED',
}
// prettier-ignore
@ -34,8 +41,8 @@ const projectActions = {
createAction(ProjectsActionTypes.GET_PROJECTS_SUCCESS, { array, previews, count })
),
getProjectsFailed: (error: any) => createAction(ProjectsActionTypes.GET_PROJECTS_FAILED, { error }),
updateProjectsGettingQuery: (query: Partial<ProjectsQuery>) => (
createAction(ProjectsActionTypes.UPDATE_PROJECTS_GETTING_QUERY, { query })
updateProjectsGettingQuery: (query: Partial<ProjectsQuery>, tasksQuery: Partial<TasksQuery> = {}) => (
createAction(ProjectsActionTypes.UPDATE_PROJECTS_GETTING_QUERY, { query, tasksQuery })
),
createProject: () => createAction(ProjectsActionTypes.CREATE_PROJECT),
createProjectSuccess: (projectId: number) => (
@ -54,14 +61,45 @@ const projectActions = {
deleteProjectFailed: (projectId: number, error: any) => (
createAction(ProjectsActionTypes.DELETE_PROJECT_FAILED, { projectId, error })
),
backupProject: (projectId: number) => createAction(ProjectsActionTypes.BACKUP_PROJECT, { projectId }),
backupProjectSuccess: (projectID: number) => (
createAction(ProjectsActionTypes.BACKUP_PROJECT_SUCCESS, { projectID })
),
backupProjectFailed: (projectID: number, error: any) => (
createAction(ProjectsActionTypes.BACKUP_PROJECT_FAILED, { projectId: projectID, error })
),
restoreProject: () => createAction(ProjectsActionTypes.RESTORE_PROJECT),
restoreProjectSuccess: (projectID: number) => (
createAction(ProjectsActionTypes.RESTORE_PROJECT_SUCCESS, { projectID })
),
restoreProjectFailed: (error: any) => (
createAction(ProjectsActionTypes.RESTORE_PROJECT_FAILED, { error })
),
};
export type ProjectActions = ActionUnion<typeof projectActions>;
export function getProjectsAsync(query: Partial<ProjectsQuery>): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>, getState): Promise<void> => {
export function getProjectTasksAsync(tasksQuery: Partial<TasksQuery> = {}): ThunkAction<void> {
return (dispatch: ActionCreator<Dispatch>): void => {
const store = getCVATStore();
const state: CombinedState = store.getState();
dispatch(projectActions.updateProjectsGettingQuery({}, tasksQuery));
const query: Partial<TasksQuery> = {
...state.projects.tasksGettingQuery,
page: 1,
...tasksQuery,
};
dispatch(getTasksAsync(query));
};
}
export function getProjectsAsync(
query: Partial<ProjectsQuery>, tasksQuery: Partial<TasksQuery> = {},
): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch(projectActions.getProjects());
dispatch(projectActions.updateProjectsGettingQuery(query));
dispatch(projectActions.updateProjectsGettingQuery(query, tasksQuery));
// Clear query object from null fields
const filteredQuery: Partial<ProjectsQuery> = {
@ -75,6 +113,23 @@ export function getProjectsAsync(query: Partial<ProjectsQuery>): ThunkAction {
}
}
// Temporary hack to do not change UI currently for projects
// Will be redesigned in a different PR
const filter = {
and: ['owner', 'assignee', 'name', 'status'].reduce<object[]>((acc, filterField) => {
if (filterField in filteredQuery) {
acc.push({ '==': [{ var: filterField }, filteredQuery[filterField]] });
delete filteredQuery[filterField];
}
return acc;
}, []),
};
if (filter.and.length) {
filteredQuery.filter = JSON.stringify(filter);
}
let result = null;
try {
result = await cvat.projects.get(filteredQuery);
@ -85,38 +140,15 @@ export function getProjectsAsync(query: Partial<ProjectsQuery>): ThunkAction {
const array = Array.from(result);
// Appropriate tasks fetching proccess needs with retrieving only a single project
if (Object.keys(filteredQuery).includes('id')) {
const tasks: any[] = [];
const [project] = array;
const taskPreviewPromises: Promise<string>[] = (project as any).tasks.map((task: any): string => {
tasks.push(task);
return (task as any).frames.preview().catch(() => '');
});
const taskPreviews = await Promise.all(taskPreviewPromises);
const state = getState();
const previewPromises = array.map((project): string => (project as any).preview().catch(() => ''));
dispatch(projectActions.getProjectsSuccess(array, await Promise.all(previewPromises), result.count));
dispatch(projectActions.getProjectsSuccess(array, taskPreviews, result.count));
if (!state.tasks.fetching) {
dispatch(
getTasksSuccess(tasks, taskPreviews, tasks.length, {
page: 1,
assignee: null,
id: null,
mode: null,
name: null,
owner: null,
search: null,
status: null,
}),
);
}
} else {
const previewPromises = array.map((project): string => (project as any).preview().catch(() => ''));
dispatch(projectActions.getProjectsSuccess(array, await Promise.all(previewPromises), result.count));
// Appropriate tasks fetching proccess needs with retrieving only a single project
if (Object.keys(filteredQuery).includes('id') && typeof filteredQuery.id === 'number') {
dispatch(getProjectTasksAsync({
...tasksQuery,
projectId: filteredQuery.id,
}));
}
};
}
@ -136,17 +168,14 @@ export function createProjectAsync(data: any): ThunkAction {
}
export function updateProjectAsync(projectInstance: any): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
return async (dispatch, getState): Promise<void> => {
try {
const state = getState();
dispatch(projectActions.updateProject());
await projectInstance.save();
const [project] = await cvat.projects.get({ id: projectInstance.id });
// TODO: Check case when a project is not available anymore after update
// (assignee changes assignee and project is not public)
dispatch(projectActions.updateProjectSuccess(project));
project.tasks.forEach((task: any) => {
dispatch(updateTaskSuccess(task, task.id));
});
dispatch(getProjectTasksAsync(state.projects.tasksGettingQuery));
} catch (error) {
let project = null;
try {
@ -171,3 +200,31 @@ export function deleteProjectAsync(projectInstance: any): ThunkAction {
}
};
}
export function restoreProjectAsync(file: File): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch(projectActions.restoreProject());
try {
const projectInstance = await cvat.classes.Project.restore(file);
dispatch(projectActions.restoreProjectSuccess(projectInstance));
} catch (error) {
dispatch(projectActions.restoreProjectFailed(error));
}
};
}
export function backupProjectAsync(projectInstance: any): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch(projectActions.backupProject(projectInstance.id));
try {
const url = await projectInstance.backup();
const downloadAnchor = window.document.getElementById('downloadAnchor') as HTMLAnchorElement;
downloadAnchor.href = url;
downloadAnchor.click();
dispatch(projectActions.backupProjectSuccess(projectInstance.id));
} catch (error) {
dispatch(projectActions.backupProjectFailed(projectInstance.id, error));
}
};
}

@ -4,13 +4,10 @@
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import getCore from 'cvat-core-wrapper';
import { updateTaskSuccess } from './tasks-actions';
const cvat = getCore();
export enum ReviewActionTypes {
INITIALIZE_REVIEW_SUCCESS = 'INITIALIZE_REVIEW_SUCCESS',
INITIALIZE_REVIEW_FAILED = 'INITIALIZE_REVIEW_FAILED',
CREATE_ISSUE = 'CREATE_ISSUE',
START_ISSUE = 'START_ISSUE',
FINISH_ISSUE_SUCCESS = 'FINISH_ISSUE_SUCCESS',
@ -25,17 +22,16 @@ export enum ReviewActionTypes {
COMMENT_ISSUE = 'COMMENT_ISSUE',
COMMENT_ISSUE_SUCCESS = 'COMMENT_ISSUE_SUCCESS',
COMMENT_ISSUE_FAILED = 'COMMENT_ISSUE_FAILED',
REMOVE_ISSUE_SUCCESS = 'REMOVE_ISSUE_SUCCESS',
REMOVE_ISSUE_FAILED = 'REMOVE_ISSUE_FAILED',
SUBMIT_REVIEW = 'SUBMIT_REVIEW',
SUBMIT_REVIEW_SUCCESS = 'SUBMIT_REVIEW_SUCCESS',
SUBMIT_REVIEW_FAILED = 'SUBMIT_REVIEW_FAILED',
SWITCH_ISSUES_HIDDEN_FLAG = 'SWITCH_ISSUES_HIDDEN_FLAG',
SWITCH_RESOLVED_ISSUES_HIDDEN_FLAG = 'SWITCH_RESOLVED_ISSUES_HIDDEN_FLAG',
}
export const reviewActions = {
initializeReviewSuccess: (reviewInstance: any, frame: number) => (
createAction(ReviewActionTypes.INITIALIZE_REVIEW_SUCCESS, { reviewInstance, frame })
),
initializeReviewFailed: (error: any) => createAction(ReviewActionTypes.INITIALIZE_REVIEW_FAILED, { error }),
createIssue: () => createAction(ReviewActionTypes.CREATE_ISSUE, {}),
startIssue: (position: number[]) => (
createAction(ReviewActionTypes.START_ISSUE, { position: cvat.classes.Issue.hull(position) })
@ -54,67 +50,46 @@ export const reviewActions = {
reopenIssue: (issueId: number) => createAction(ReviewActionTypes.REOPEN_ISSUE, { issueId }),
reopenIssueSuccess: () => createAction(ReviewActionTypes.REOPEN_ISSUE_SUCCESS),
reopenIssueFailed: (error: any) => createAction(ReviewActionTypes.REOPEN_ISSUE_FAILED, { error }),
submitReview: (reviewId: number) => createAction(ReviewActionTypes.SUBMIT_REVIEW, { reviewId }),
submitReview: (jobId: number) => createAction(ReviewActionTypes.SUBMIT_REVIEW, { jobId }),
submitReviewSuccess: () => createAction(ReviewActionTypes.SUBMIT_REVIEW_SUCCESS),
submitReviewFailed: (error: any) => createAction(ReviewActionTypes.SUBMIT_REVIEW_FAILED, { error }),
submitReviewFailed: (error: any, jobId: number) => (
createAction(ReviewActionTypes.SUBMIT_REVIEW_FAILED, { error, jobId })
),
removeIssueSuccess: (issueId: number, frame: number) => (
createAction(ReviewActionTypes.REMOVE_ISSUE_SUCCESS, { issueId, frame })
),
removeIssueFailed: (error: any) => createAction(ReviewActionTypes.REMOVE_ISSUE_FAILED, { error }),
switchIssuesHiddenFlag: (hidden: boolean) => createAction(ReviewActionTypes.SWITCH_ISSUES_HIDDEN_FLAG, { hidden }),
switchIssuesHiddenResolvedFlag: (hidden: boolean) => (
createAction(ReviewActionTypes.SWITCH_RESOLVED_ISSUES_HIDDEN_FLAG, { hidden })
),
};
export type ReviewActions = ActionUnion<typeof reviewActions>;
export const initializeReviewAsync = (): ThunkAction => async (dispatch, getState) => {
try {
const state = getState();
const {
annotation: {
job: { instance: jobInstance },
player: {
frame: { number: frame },
},
},
} = state;
const reviews = await jobInstance.reviews();
const count = reviews.length;
let reviewInstance = null;
if (count && reviews[count - 1].id < 0) {
reviewInstance = reviews[count - 1];
} else {
reviewInstance = new cvat.classes.Review({ job: jobInstance.id });
}
dispatch(reviewActions.initializeReviewSuccess(reviewInstance, frame));
} catch (error) {
dispatch(reviewActions.initializeReviewFailed(error));
}
};
export const finishIssueAsync = (message: string): ThunkAction => async (dispatch, getState) => {
const state = getState();
const {
auth: { user },
annotation: {
player: {
frame: { number: frameNumber },
},
job: {
instance: jobInstance,
},
},
review: { activeReview, newIssuePosition },
review: { newIssuePosition },
} = state;
try {
const issue = await activeReview.openIssue({
const issue = new cvat.classes.Issue({
job: jobInstance.id,
frame: frameNumber,
position: newIssuePosition,
owner: user,
comment_set: [
{
message,
author: user,
},
],
});
await activeReview.toLocalStorage();
dispatch(reviewActions.finishIssueSuccess(frameNumber, issue));
const savedIssue = await jobInstance.openIssue(issue, message);
dispatch(reviewActions.finishIssueSuccess(frameNumber, savedIssue));
} catch (error) {
dispatch(reviewActions.finishIssueFailed(error));
}
@ -124,7 +99,7 @@ export const commentIssueAsync = (id: number, message: string): ThunkAction => a
const state = getState();
const {
auth: { user },
review: { frameIssues, activeReview },
review: { frameIssues },
} = state;
try {
@ -132,11 +107,9 @@ export const commentIssueAsync = (id: number, message: string): ThunkAction => a
const [issue] = frameIssues.filter((_issue: any): boolean => _issue.id === id);
await issue.comment({
message,
author: user,
owner: user,
});
if (activeReview && activeReview.issues.includes(issue)) {
await activeReview.toLocalStorage();
}
dispatch(reviewActions.commentIssueSuccess());
} catch (error) {
dispatch(reviewActions.commentIssueFailed(error));
@ -147,17 +120,13 @@ export const resolveIssueAsync = (id: number): ThunkAction => async (dispatch, g
const state = getState();
const {
auth: { user },
review: { frameIssues, activeReview },
review: { frameIssues },
} = state;
try {
dispatch(reviewActions.resolveIssue(id));
const [issue] = frameIssues.filter((_issue: any): boolean => _issue.id === id);
await issue.resolve(user);
if (activeReview && activeReview.issues.includes(issue)) {
await activeReview.toLocalStorage();
}
dispatch(reviewActions.resolveIssueSuccess());
} catch (error) {
dispatch(reviewActions.resolveIssueFailed(error));
@ -168,39 +137,35 @@ export const reopenIssueAsync = (id: number): ThunkAction => async (dispatch, ge
const state = getState();
const {
auth: { user },
review: { frameIssues, activeReview },
review: { frameIssues },
} = state;
try {
dispatch(reviewActions.reopenIssue(id));
const [issue] = frameIssues.filter((_issue: any): boolean => _issue.id === id);
await issue.reopen(user);
if (activeReview && activeReview.issues.includes(issue)) {
await activeReview.toLocalStorage();
}
dispatch(reviewActions.reopenIssueSuccess());
} catch (error) {
dispatch(reviewActions.reopenIssueFailed(error));
}
};
export const submitReviewAsync = (review: any): ThunkAction => async (dispatch, getState) => {
export const deleteIssueAsync = (id: number): ThunkAction => async (dispatch, getState) => {
const state = getState();
const {
review: { frameIssues },
annotation: {
job: { instance: jobInstance },
player: {
frame: { number: frameNumber },
},
},
} = state;
try {
dispatch(reviewActions.submitReview(review.id));
await review.submit(jobInstance.id);
const [task] = await cvat.tasks.get({ id: jobInstance.task.id });
dispatch(updateTaskSuccess(task, jobInstance.task.id));
dispatch(reviewActions.submitReviewSuccess());
const [issue] = frameIssues.filter((_issue: any): boolean => _issue.id === id);
await issue.delete();
dispatch(reviewActions.removeIssueSuccess(id, frameNumber));
} catch (error) {
dispatch(reviewActions.submitReviewFailed(error));
dispatch(reviewActions.removeIssueFailed(error));
}
};

@ -22,6 +22,10 @@ export enum SettingsActionTypes {
CHANGE_FRAME_STEP = 'CHANGE_FRAME_STEP',
CHANGE_FRAME_SPEED = 'CHANGE_FRAME_SPEED',
SWITCH_RESET_ZOOM = 'SWITCH_RESET_ZOOM',
SWITCH_SMOOTH_IMAGE = 'SWITCH_SMOOTH_IMAGE',
SWITCH_TEXT_FONT_SIZE = 'SWITCH_TEXT_FONT_SIZE',
SWITCH_TEXT_POSITION = 'SWITCH_TEXT_POSITION',
SWITCH_TEXT_CONTENT = 'SWITCH_TEXT_CONTENT',
CHANGE_BRIGHTNESS_LEVEL = 'CHANGE_BRIGHTNESS_LEVEL',
CHANGE_CONTRAST_LEVEL = 'CHANGE_CONTRAST_LEVEL',
CHANGE_SATURATION_LEVEL = 'CHANGE_SATURATION_LEVEL',
@ -166,6 +170,42 @@ export function switchResetZoom(resetZoom: boolean): AnyAction {
};
}
export function switchSmoothImage(enabled: boolean): AnyAction {
return {
type: SettingsActionTypes.SWITCH_SMOOTH_IMAGE,
payload: {
smoothImage: enabled,
},
};
}
export function switchTextFontSize(fontSize: number): AnyAction {
return {
type: SettingsActionTypes.SWITCH_TEXT_FONT_SIZE,
payload: {
fontSize,
},
};
}
export function switchTextPosition(position: 'auto' | 'center'): AnyAction {
return {
type: SettingsActionTypes.SWITCH_TEXT_POSITION,
payload: {
position,
},
};
}
export function switchTextContent(textContent: string): AnyAction {
return {
type: SettingsActionTypes.SWITCH_TEXT_CONTENT,
payload: {
textContent,
},
};
}
export function changeBrightnessLevel(level: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_BRIGHTNESS_LEVEL,

@ -1,4 +1,4 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -28,6 +28,9 @@ export enum TasksActionTypes {
UPDATE_TASK = 'UPDATE_TASK',
UPDATE_TASK_SUCCESS = 'UPDATE_TASK_SUCCESS',
UPDATE_TASK_FAILED = 'UPDATE_TASK_FAILED',
UPDATE_JOB = 'UPDATE_JOB',
UPDATE_JOB_SUCCESS = 'UPDATE_JOB_SUCCESS',
UPDATE_JOB_FAILED = 'UPDATE_JOB_FAILED',
HIDE_EMPTY_TASKS = 'HIDE_EMPTY_TASKS',
EXPORT_TASK = 'EXPORT_TASK',
EXPORT_TASK_SUCCESS = 'EXPORT_TASK_SUCCESS',
@ -38,36 +41,34 @@ export enum TasksActionTypes {
SWITCH_MOVE_TASK_MODAL_VISIBLE = 'SWITCH_MOVE_TASK_MODAL_VISIBLE',
}
function getTasks(): AnyAction {
function getTasks(query: TasksQuery): AnyAction {
const action = {
type: TasksActionTypes.GET_TASKS,
payload: {},
payload: {
query,
},
};
return action;
}
export function getTasksSuccess(array: any[], previews: string[], count: number, query: TasksQuery): AnyAction {
export function getTasksSuccess(array: any[], previews: string[], count: number): AnyAction {
const action = {
type: TasksActionTypes.GET_TASKS_SUCCESS,
payload: {
previews,
array,
count,
query,
},
};
return action;
}
function getTasksFailed(error: any, query: TasksQuery): AnyAction {
function getTasksFailed(error: any): AnyAction {
const action = {
type: TasksActionTypes.GET_TASKS_FAILED,
payload: {
error,
query,
},
payload: { error },
};
return action;
@ -75,7 +76,7 @@ function getTasksFailed(error: any, query: TasksQuery): AnyAction {
export function getTasksAsync(query: TasksQuery): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch(getTasks());
dispatch(getTasks(query));
// We need remove all keys with null values from query
const filteredQuery = { ...query };
@ -85,11 +86,28 @@ export function getTasksAsync(query: TasksQuery): ThunkAction<Promise<void>, {},
}
}
// Temporary hack to do not change UI currently for tasks
// Will be redesigned in a different PR
const filter = {
and: ['owner', 'assignee', 'name', 'status', 'mode', 'dimension'].reduce<object[]>((acc, filterField) => {
if (filterField in filteredQuery) {
acc.push({ '==': [{ var: filterField }, filteredQuery[filterField]] });
delete filteredQuery[filterField];
}
return acc;
}, []),
};
if (filter.and.length) {
filteredQuery.filter = JSON.stringify(filter);
}
let result = null;
try {
result = await cvat.tasks.get(filteredQuery);
} catch (error) {
dispatch(getTasksFailed(error, query));
dispatch(getTasksFailed(error));
return;
}
@ -98,7 +116,7 @@ export function getTasksAsync(query: TasksQuery): ThunkAction<Promise<void>, {},
dispatch(getInferenceStatusAsync());
dispatch(getTasksSuccess(array, await Promise.all(promises), result.count, query));
dispatch(getTasksSuccess(array, await Promise.all(promises), result.count));
};
}
@ -248,7 +266,7 @@ export function exportTaskAsync(taskInstance: any): ThunkAction<Promise<void>, {
downloadAnchor.click();
dispatch(exportTaskSuccess(taskInstance.id));
} catch (error) {
dispatch(exportTaskFailed(taskInstance.id, error));
dispatch(exportTaskFailed(taskInstance.id, error as Error));
}
};
}
@ -351,6 +369,7 @@ export function createTaskAsync(data: any): ThunkAction<Promise<void>, {}, {}, A
image_quality: 70,
use_zip_chunks: data.advanced.useZipChunks,
use_cache: data.advanced.useCache,
sorting_method: data.advanced.sortingMethod,
};
if (data.projectId) {
@ -411,8 +430,8 @@ export function createTaskAsync(data: any): ThunkAction<Promise<void>, {}, {}, A
dispatch(createTask());
try {
const savedTask = await taskInstance.save((status: string): void => {
dispatch(createTaskUpdateStatus(status));
const savedTask = await taskInstance.save((status: string, progress: number): void => {
dispatch(createTaskUpdateStatus(status + (progress !== null ? ` ${Math.floor(progress * 100)}%` : '')));
});
dispatch(createTaskSuccess(savedTask.id));
} catch (error) {
@ -439,6 +458,33 @@ export function updateTaskSuccess(task: any, taskID: number): AnyAction {
return action;
}
function updateJob(): AnyAction {
const action = {
type: TasksActionTypes.UPDATE_JOB,
payload: { },
};
return action;
}
function updateJobSuccess(jobInstance: any): AnyAction {
const action = {
type: TasksActionTypes.UPDATE_JOB_SUCCESS,
payload: { jobInstance },
};
return action;
}
function updateJobFailed(jobID: number, error: any): AnyAction {
const action = {
type: TasksActionTypes.UPDATE_JOB_FAILED,
payload: { jobID, error },
};
return action;
}
function updateTaskFailed(error: any, task: any): AnyAction {
const action = {
type: TasksActionTypes.UPDATE_TASK_FAILED,
@ -449,17 +495,11 @@ function updateTaskFailed(error: any, task: any): AnyAction {
}
export function updateTaskAsync(taskInstance: any): ThunkAction<Promise<void>, CombinedState, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>, getState: () => CombinedState): Promise<void> => {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
dispatch(updateTask());
const currentUser = getState().auth.user;
await taskInstance.save();
const nextUser = getState().auth.user;
const userFetching = getState().auth.fetching;
if (!userFetching && nextUser && currentUser.username === nextUser.username) {
const [task] = await cvat.tasks.get({ id: taskInstance.id });
dispatch(updateTaskSuccess(task, taskInstance.id));
}
const task = await taskInstance.save();
dispatch(updateTaskSuccess(task, taskInstance.id));
} catch (error) {
// try abort all changes
let task = null;
@ -480,21 +520,11 @@ export function updateTaskAsync(taskInstance: any): ThunkAction<Promise<void>, C
export function updateJobAsync(jobInstance: any): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
dispatch(updateTask());
await jobInstance.save();
const [task] = await cvat.tasks.get({ id: jobInstance.task.id });
dispatch(updateTaskSuccess(task, jobInstance.task.id));
dispatch(updateJob());
const newJob = await jobInstance.save();
dispatch(updateJobSuccess(newJob));
} catch (error) {
// try abort all changes
let task = null;
try {
[task] = await cvat.tasks.get({ id: jobInstance.task.id });
} catch (fetchError) {
dispatch(updateTaskFailed(error, jobInstance.task));
return;
}
dispatch(updateTaskFailed(error, task));
dispatch(updateJobFailed(jobInstance.id, error));
}
};
}

@ -1 +0,0 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M25.9 23c-1.73 0-2.561 1-5.4 1-2.839 0-3.664-1-5.4-1-4.472 0-8.1 3.762-8.1 8.4V33c0 1.656 1.296 3 2.893 3h21.214C32.704 36 34 34.656 34 33v-1.6c0-4.637-3.628-8.4-8.1-8.4zm5.207 10H9.893v-1.6c0-2.975 2.338-5.4 5.207-5.4.88 0 2.308 1 5.4 1 3.116 0 4.514-1 5.4-1 2.869 0 5.207 2.425 5.207 5.4V33zM20.5 22c4.791 0 8.679-4.031 8.679-9S25.29 4 20.5 4s-8.679 4.031-8.679 9 3.888 9 8.679 9zm0-15c3.188 0 5.786 2.694 5.786 6s-2.598 6-5.786 6c-3.188 0-5.786-2.694-5.786-6s2.598-6 5.786-6z" fill="#000" fill-rule="nonzero"/></svg>

Before

Width:  |  Height:  |  Size: 591 B

@ -0,0 +1,22 @@
<!-- Downloaded from: https://www.svgrepo.com/svg/253277/vector-ellipse, CC0 License -->
<svg width="40" height="40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<g>
<path d="M503.47,231.292h-19.628c-22.657-62.638-105.58-108.515-210.691-116.371V94.806c0-4.709-3.822-8.53-8.53-8.53h-51.182
c-4.709,0-8.53,3.822-8.53,8.53v21.138C87.138,128.714,0,193.264,0,269.678c0,86.046,111.014,156.046,247.465,156.046
c127.776,0,221.943-50.389,237.766-126.189h18.238c4.709,0,8.53-3.822,8.53-8.53v-51.182
C512,235.113,508.187,231.292,503.47,231.292z M221.968,103.337h34.121v34.121h-34.121V103.337z M247.465,408.663
c-127.051,0-230.405-62.348-230.405-138.985c0-67.236,79.742-124.355,187.847-136.571v12.881c0,4.709,3.822,8.53,8.53,8.53h51.182
c4.709,0,8.53-3.822,8.53-8.53v-13.964c94.508,7.328,169.336,46.072,192.556,99.268h-13.41c-4.709,0-8.53,3.822-8.53,8.53v51.182
c0,4.709,3.822,8.53,8.53,8.53h15.235C451.025,364.246,362.821,408.663,247.465,408.663z M494.939,282.474h-34.121v-34.121h34.121
V282.474z"/>
</g>
</g>
<g>
<g>
<path d="M256.09,256.883h-8.53v-8.53c0-4.709-3.813-8.53-8.53-8.53c-4.709,0-8.53,3.822-8.53,8.53v8.53h-8.53
c-4.709,0-8.53,3.822-8.53,8.53c0,4.709,3.822,8.53,8.53,8.53h8.53v8.53c0,4.709,3.822,8.53,8.53,8.53s8.53-3.822,8.53-8.53v-8.53
h8.53c4.709,0,8.53-3.822,8.53-8.53C264.62,260.704,260.798,256.883,256.09,256.883z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -1 +0,0 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" stroke="#000" stroke-width="2" fill="none"><path d="M3 9h34v22H3z"/><path d="M33.626 16.983h-2.571v-5.538h-3.858v5.538h-2.571l4.5 6.462z"/></g></svg>

Before

Width:  |  Height:  |  Size: 235 B

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- The icon received from: https://github.com/gilbarbara/logos -->
<!-- License: CC0-1.0 License -->
<svg width="1em" height="1em" viewBox="0 0 256 206" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<path d="M170.2517,56.8186 L192.5047,34.5656 L193.9877,25.1956 C153.4367,-11.6774 88.9757,-7.4964 52.4207,33.9196 C42.2667,45.4226 34.7337,59.7636 30.7167,74.5726 L38.6867,73.4496 L83.1917,66.1106 L86.6277,62.5966 C106.4247,40.8546 139.8977,37.9296 162.7557,56.4286 L170.2517,56.8186 Z" fill="#EA4335"></path>
<path d="M224.2048,73.9182 C219.0898,55.0822 208.5888,38.1492 193.9878,25.1962 L162.7558,56.4282 C175.9438,67.2042 183.4568,83.4382 183.1348,100.4652 L183.1348,106.0092 C198.4858,106.0092 210.9318,118.4542 210.9318,133.8052 C210.9318,149.1572 198.4858,161.2902 183.1348,161.2902 L127.4638,161.2902 L121.9978,167.2242 L121.9978,200.5642 L127.4638,205.7952 L183.1348,205.7952 C223.0648,206.1062 255.6868,174.3012 255.9978,134.3712 C256.1858,110.1682 244.2528,87.4782 224.2048,73.9182" fill="#4285F4"></path>
<path d="M71.8704,205.7957 L127.4634,205.7957 L127.4634,161.2897 L71.8704,161.2897 C67.9094,161.2887 64.0734,160.4377 60.4714,158.7917 L52.5844,161.2117 L30.1754,183.4647 L28.2234,191.0387 C40.7904,200.5277 56.1234,205.8637 71.8704,205.7957" fill="#34A853"></path>
<path d="M71.8704,61.4255 C31.9394,61.6635 -0.2366,94.2275 0.0014,134.1575 C0.1344,156.4555 10.5484,177.4455 28.2234,191.0385 L60.4714,158.7915 C46.4804,152.4705 40.2634,136.0055 46.5844,122.0155 C52.9044,108.0255 69.3704,101.8085 83.3594,108.1285 C89.5244,110.9135 94.4614,115.8515 97.2464,122.0155 L129.4944,89.7685 C115.7734,71.8315 94.4534,61.3445 71.8704,61.4255" fill="#FBBC05"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@ -1 +0,0 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M20.5 11c2.475 0 4.5-2.025 4.5-4.5S22.975 2 20.5 2A4.513 4.513 0 0 0 16 6.5c0 2.475 2.025 4.5 4.5 4.5zm0 4.5A4.513 4.513 0 0 0 16 20c0 2.475 2.025 4.5 4.5 4.5S25 22.475 25 20s-2.025-4.5-4.5-4.5zm0 13.5a4.513 4.513 0 0 0-4.5 4.5c0 2.475 2.025 4.5 4.5 4.5s4.5-2.025 4.5-4.5-2.025-4.5-4.5-4.5z" fill="#000" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 403 B

@ -1 +0,0 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M20 28.656c-4.267 0-7.72-3.325-8.038-7.54l-5.9-4.591c-.777.98-1.49 2.016-2.066 3.149a1.844 1.844 0 0 0 0 1.653C7.046 27.32 13.085 31.375 20 31.375c1.514 0 2.974-.227 4.381-.593l-2.919-2.274a8.053 8.053 0 0 1-1.462.148zm17.652 3.291l-6.218-4.84a18.74 18.74 0 0 0 4.57-5.78 1.844 1.844 0 0 0 0-1.654C32.954 13.68 26.914 9.625 20 9.625a17.24 17.24 0 0 0-8.287 2.135L4.557 6.191a.896.896 0 0 0-1.263.16L2.189 7.78a.91.91 0 0 0 .159 1.272l33.095 25.756a.896.896 0 0 0 1.263-.16l1.105-1.43a.91.91 0 0 0-.159-1.272zm-10.334-8.043l-2.21-1.72a5.38 5.38 0 0 0-1.812-6.027 5.3 5.3 0 0 0-4.72-.88c.34.463.523 1.023.524 1.598a2.659 2.659 0 0 1-.087.566l-4.14-3.222A7.972 7.972 0 0 1 20 12.344a8.067 8.067 0 0 1 5.729 2.387 8.18 8.18 0 0 1 2.37 5.769c0 1.225-.297 2.367-.781 3.405z" fill="#000" fill-rule="nonzero"/></svg>

Before

Width:  |  Height:  |  Size: 880 B

@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" height="1em" width="1em" viewBox="64 64 896 896" style="transform: scale(1.5)">
<g style="transform: scale(25)">
<g transform="translate(3 7)" fill-rule="evenodd">
<rect x="5.75" y="4.5" width="22.5" height="15.75" rx="2.25"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 359 B

@ -1 +0,0 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M20.5 3c4.885 0 8.857 3.812 8.857 8.5 0 4.688-3.972 8.5-8.857 8.5s-8.857-3.812-8.857-8.5c0-4.688 3.972-8.5 8.857-8.5zM36 34.45c0 1.408-1.384 2.55-3.1 2.55H8.1C6.384 37 5 35.858 5 34.45V31.9c0-4.223 4.166-7.65 9.3-7.65h.692a14.802 14.802 0 0 0 11.016 0h.692c5.134 0 9.3 3.427 9.3 7.65v2.55z" fill="#000" fill-rule="nonzero"/></svg>

Before

Width:  |  Height:  |  Size: 402 B

@ -0,0 +1,14 @@
<svg width="120" height="28" viewBox="0 -3 120 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M117.045 0.566335H116.582V0.264099H117.898V0.566335H117.435V1.97881H117.044V0.566335H117.045Z" fill="black"/>
<path d="M118.188 0.26413H118.7L119.101 1.33112L119.496 0.26413H120V1.97884H119.614V0.77498L119.139 1.97884H119.012L118.535 0.77498V1.97884H118.187V0.26413H118.188Z" fill="black"/>
<path d="M23.0686 17.0748V24.4231H20.4009V6.05237H23.0686V7.81389C23.9997 6.58052 25.5852 5.80051 27.2714 5.80051C30.568 5.80051 33.5375 8.31711 33.5375 12.3434C33.5375 16.3447 30.4673 18.8862 27.2963 18.8862C25.5598 18.8867 23.9748 18.2324 23.0686 17.0748ZM30.8698 12.3184C30.8698 10.0283 29.1332 8.2418 26.8939 8.2418C24.6037 8.2418 22.918 10.0791 22.918 12.3184C22.918 14.5832 24.6042 16.4205 26.8939 16.4205C29.1337 16.4205 30.8698 14.6086 30.8698 12.3184Z" fill="black"/>
<path d="M34.778 12.3943C34.778 8.69466 37.6217 5.80103 41.3208 5.80103C45.0453 5.80103 47.7884 8.49368 47.7884 12.2182V13.275H37.3704C37.7225 15.2126 39.2326 16.4963 41.4221 16.4963C43.1332 16.4963 44.4419 15.5901 45.0962 14.2316L47.3106 15.465C46.2034 17.5287 44.2149 18.8878 41.4221 18.8878C37.4457 18.8867 34.778 16.043 34.778 12.3943ZM37.496 10.9848H45.0458C44.6434 9.17294 43.2589 8.16651 41.3213 8.16651C39.4585 8.16651 37.9993 9.32406 37.496 10.9848Z" fill="black"/>
<path d="M49.409 6.05237H52.0767V7.68821C52.9325 6.53066 54.2916 5.80051 55.9524 5.80051C59.1233 5.80051 61.0609 7.86426 61.0609 11.2866V18.6349H58.3428V11.5135C58.3428 9.44973 57.3109 8.14106 55.2975 8.14106C53.4856 8.14106 52.1016 9.5001 52.1016 11.7145V18.6349H49.409V6.05237Z" fill="black"/>
<path d="M60.3419 0.264099H63.1856L68.1435 14.5832L73.1009 0.264099H75.9447L69.5275 18.6349H66.7595L60.3419 0.264099Z" fill="black"/>
<path d="M76.9639 0.264099H79.7069V18.6349H76.9639V0.264099Z" fill="black"/>
<path d="M82.2078 0.264099H85.3034L94.6147 14.1558V0.264099H97.3323V18.6349H94.4382L84.9508 4.51729V18.6349H82.2078V0.264099Z" fill="black"/>
<path d="M12.9117 7.96652C12.6675 8.46617 12.1663 8.81369 11.5633 8.81369C10.7172 8.81369 10.0583 8.12629 10.0583 7.28572C10.0583 6.6782 10.4048 6.15208 10.9151 5.9048C10.4587 5.70738 9.95395 5.59747 9.41817 5.59747C7.302 5.59747 5.65497 7.32999 5.65497 9.4319C5.65497 11.5338 7.30251 13.2516 9.41817 13.2516C11.5481 13.2516 13.1956 11.5333 13.1956 9.4319C13.1951 8.91393 13.0938 8.41936 12.9117 7.96652Z" fill="black"/>
<path d="M0 9.437C0 4.22775 4.20281 0 9.41206 0C14.6468 0 18.8491 4.22775 18.8491 9.437C18.8491 14.6462 14.6462 18.874 9.41206 18.874C4.20281 18.874 0 14.6462 0 9.437ZM16.106 9.437C16.106 5.71247 13.187 2.64228 9.41206 2.64228C5.66261 2.64228 2.74302 5.71247 2.74302 9.437C2.74302 13.1615 5.6621 16.2063 9.41206 16.2063C13.187 16.2068 16.106 13.1615 16.106 9.437Z" fill="black"/>
<path d="M111.862 7.97924C111.618 8.4789 111.117 8.82642 110.514 8.82642C109.668 8.82642 109.008 8.13901 109.008 7.29845C109.008 6.69092 109.355 6.16481 109.865 5.91752C109.409 5.7201 108.904 5.6102 108.368 5.6102C106.252 5.6102 104.605 7.34272 104.605 9.44463C104.605 11.5465 106.252 13.2648 108.368 13.2648C110.498 13.2648 112.145 11.5465 112.145 9.44463C112.145 8.92666 112.044 8.43158 111.862 7.97924Z" fill="black"/>
<path d="M98.9503 9.44972C98.9503 4.24047 103.153 0.0127258 108.362 0.0127258C113.597 0.0127258 117.799 4.24047 117.799 9.44972C117.799 14.659 113.596 18.8867 108.362 18.8867C103.153 18.8867 98.9503 14.659 98.9503 9.44972ZM115.056 9.44972C115.056 5.72519 112.137 2.655 108.362 2.655C104.613 2.655 101.693 5.72519 101.693 9.44972C101.693 13.1743 104.612 16.219 108.362 16.219C112.137 16.219 115.056 13.1743 115.056 9.44972Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

@ -1 +0,0 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M19.258 20.746l.049 13.82a.7.7 0 0 0 .697.698.687.687 0 0 0 .692-.693l-.048-13.924 13.924.049a.687.687 0 0 0 .692-.692.7.7 0 0 0-.698-.698l-13.82-.048-.048-13.83a.7.7 0 0 0-.697-.697.69.69 0 0 0-.692.693l.049 13.933-13.934-.048a.69.69 0 0 0-.692.692.7.7 0 0 0 .697.697l13.83.048z" stroke="#000" stroke-width="2" fill="#000" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 423 B

@ -1 +0,0 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M22.147 2C23.169 2 24 2.831 24 3.853h0v3.368c0 .542.298.988.798 1.195a1.257 1.257 0 0 0 1.41-.28h0l2.38-2.381c.701-.702 1.922-.7 2.622 0h0l3.035 3.035c.35.35.543.816.543 1.311s-.193.961-.543 1.311h0l-2.381 2.38c-.382.383-.487.91-.28 1.41.207.5.653.798 1.195.798h3.368c1.022 0 1.853.831 1.853 1.853h0v4.294A1.855 1.855 0 0 1 36.147 24h0-3.368c-.542 0-.989.298-1.196.798a1.26 1.26 0 0 0 .28 1.41h0l2.382 2.38c.35.35.542.816.542 1.31 0 .496-.192.962-.542 1.312h0l-3.036 3.035c-.7.7-1.92.702-2.622 0h0l-2.38-2.381a1.257 1.257 0 0 0-1.41-.28c-.5.207-.798.653-.798 1.195h0v3.368A1.855 1.855 0 0 1 22.146 38h0-4.293A1.855 1.855 0 0 1 16 36.147h0v-3.368c0-.542-.298-.988-.798-1.195a1.258 1.258 0 0 0-1.41.28h0l-2.38 2.381c-.701.702-1.921.701-2.622 0h0L5.755 31.21a1.844 1.844 0 0 1-.543-1.311c0-.495.193-.961.543-1.311h0l2.381-2.38c.382-.383.487-.91.28-1.41A1.257 1.257 0 0 0 7.221 24h0-3.368A1.855 1.855 0 0 1 2 22.146h0v-4.293C2 16.831 2.831 16 3.853 16h3.368c.542 0 .988-.298 1.195-.798a1.26 1.26 0 0 0-.28-1.41h0l-2.381-2.38a1.843 1.843 0 0 1-.543-1.31c0-.496.193-.962.543-1.312h0L8.79 5.755c.7-.7 1.92-.702 2.622 0h0l2.38 2.38c.383.382.911.49 1.41.281.5-.207.798-.653.798-1.195h0V3.853C16 2.831 16.831 2 17.853 2h0zm-.001 1.333h-4.293a.52.52 0 0 0-.52.52h0v3.368a2.584 2.584 0 0 1-1.621 2.426 2.586 2.586 0 0 1-2.863-.569h0l-2.38-2.38a.52.52 0 0 0-.736 0h0L6.697 9.732a.52.52 0 0 0 0 .736h0l2.382 2.38c.766.766.984 1.863.569 2.863a2.586 2.586 0 0 1-2.427 1.621h0-3.368a.52.52 0 0 0-.52.52h0v4.294c0 .286.234.52.52.52h3.368c1.083 0 2.013.621 2.427 1.62.415 1 .196 2.098-.57 2.863h0l-2.38 2.38a.52.52 0 0 0 0 .737h0l3.035 3.035a.52.52 0 0 0 .736 0h0l2.38-2.381a2.59 2.59 0 0 1 2.863-.569 2.586 2.586 0 0 1 1.621 2.427h0v3.368c0 .286.234.52.52.52h4.294a.52.52 0 0 0 .52-.52h0v-3.368c0-1.083.621-2.013 1.621-2.427 1-.413 2.097-.197 2.863.57h0l2.38 2.38a.52.52 0 0 0 .736 0h0l3.036-3.035a.52.52 0 0 0 0-.736h0l-2.382-2.38a2.585 2.585 0 0 1-.569-2.863 2.586 2.586 0 0 1 2.427-1.621h3.368a.52.52 0 0 0 .52-.52h0v-4.294a.52.52 0 0 0-.52-.519h0-3.368a2.586 2.586 0 0 1-2.427-1.621c-.415-1-.196-2.098.57-2.863h0l2.38-2.38a.52.52 0 0 0 0-.737h0l-3.035-3.035a.52.52 0 0 0-.736 0h0l-2.38 2.38a2.585 2.585 0 0 1-2.863.57 2.586 2.586 0 0 1-1.621-2.427h0V3.853a.52.52 0 0 0-.521-.52h0zM20 14c3.309 0 6 2.691 6 6s-2.691 6-6 6-6-2.691-6-6 2.691-6 6-6zm0 1.333A4.673 4.673 0 0 0 15.333 20 4.673 4.673 0 0 0 20 24.667 4.673 4.673 0 0 0 24.667 20 4.673 4.673 0 0 0 20 15.333z" stroke="#000" fill="#000" fill-rule="nonzero"/></svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

@ -1 +0,0 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" stroke="#000" stroke-width="2" fill="none"><path d="M3 9h34v22H3z"/><path d="M28.5 9H37v22h-8.5z"/></g></svg>

Before

Width:  |  Height:  |  Size: 195 B

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

Loading…
Cancel
Save