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

@ -1,5 +1,5 @@
<!--- <!---
Copyright (C) 2020 Intel Corporation Copyright (C) 2020-2021 Intel Corporation
SPDX-License-Identifier: MIT SPDX-License-Identifier: MIT
--> -->
@ -24,7 +24,7 @@ current behavior -->
to implement the addition or change --> to implement the addition or change -->
### Steps to Reproduce (for bugs) ### 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 --> reproduce this bug. Include code to reproduce, if relevant -->
1. 1.
1. 1.

@ -1,11 +1,11 @@
<!--- <!---
Copyright (C) 2020-2021 Intel Corporation Copyright (C) 2020-2022 Intel Corporation
SPDX-License-Identifier: MIT SPDX-License-Identifier: MIT
--> -->
<!-- Raised an issue to propose your change (https://github.com/opencv/cvat/issues). <!-- 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. 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) Read the [CONTRIBUTION](https://github.com/opencv/cvat/blob/develop/CONTRIBUTING.md)
guide. --> guide. -->
@ -25,10 +25,10 @@ see how your change affects other areas of the code, etc. -->
### Checklist ### Checklist
<!-- Go over all the following points, and put an `x` in all the boxes that apply. <!-- 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 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! --> 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 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]( - [ ] I have updated the [documentation](
https://github.com/opencv/cvat/blob/develop/README.md#documentation) accordingly https://github.com/opencv/cvat/blob/develop/README.md#documentation) accordingly
- [ ] I have added tests to cover my changes - [ ] 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) - [ ] I have updated the license header for each file (see an example below)
```python ```python
# Copyright (C) 2021 Intel Corporation # Copyright (C) 2022 Intel Corporation
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
``` ```

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

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

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

@ -16,6 +16,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.8'
- name: Getting SHA from the default branch - name: Getting SHA from the default branch
id: get-sha id: get-sha
run: | run: |
@ -63,6 +66,20 @@ jobs:
cache-from: type=local,src=/tmp/cvat_cache_server cache-from: type=local,src=/tmp/cvat_cache_server
tags: openvino/cvat_server:latest tags: openvino/cvat_server:latest
load: true 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 - name: Running unit tests
env: env:
HOST_COVERAGE_DATA_DIR: ${{ github.workspace }} HOST_COVERAGE_DATA_DIR: ${{ github.workspace }}
@ -88,7 +105,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: 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: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Getting SHA from the default branch - name: Getting SHA from the default branch
@ -165,25 +182,25 @@ jobs:
DJANGO_SU_NAME: 'admin' DJANGO_SU_NAME: 'admin'
DJANGO_SU_EMAIL: 'admin@localhost.company' DJANGO_SU_EMAIL: 'admin@localhost.company'
DJANGO_SU_PASSWORD: '12qwaszx' DJANGO_SU_PASSWORD: '12qwaszx'
API_ABOUT_PAGE: "localhost:8080/api/v1/server/about" API_ABOUT_PAGE: "localhost:8080/api/server/about"
run: | 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 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" 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 cd ./tests
npm ci npm ci
if [[ ${{ github.ref }} == 'refs/heads/develop' ]]; then if [[ ${{ github.ref }} == 'refs/heads/develop' ]]; then
if [ ${{ matrix.specs }} == 'canvas3d_functionality' ] || [ ${{ matrix.specs }} == 'canvas3d_functionality_2' ]; 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 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 fi
mv ./.nyc_output/out.json ./.nyc_output/out_${{ matrix.specs }}.json mv ./.nyc_output/out.json ./.nyc_output/out_${{ matrix.specs }}.json
else else
if [ ${{ matrix.specs }} == 'canvas3d_functionality' ] || [ ${{ matrix.specs }} == 'canvas3d_functionality_2' ]; then 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 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
fi fi
- name: Creating a log file from "cvat" container logs - name: Creating a log file from "cvat" container logs

@ -28,11 +28,11 @@ jobs:
DJANGO_SU_NAME: 'admin' DJANGO_SU_NAME: 'admin'
DJANGO_SU_EMAIL: 'admin@localhost.company' DJANGO_SU_EMAIL: 'admin@localhost.company'
DJANGO_SU_PASSWORD: '12qwaszx' DJANGO_SU_PASSWORD: '12qwaszx'
API_ABOUT_PAGE: "localhost:8080/api/v1/server/about" API_ABOUT_PAGE: "localhost:8080/api/server/about"
run: | run: |
docker-compose -f docker-compose.yml -f docker-compose.dev.yml build 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 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" 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 cd ./tests
npm ci npm ci

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

@ -16,10 +16,10 @@ jobs:
DJANGO_SU_NAME: "admin" DJANGO_SU_NAME: "admin"
DJANGO_SU_EMAIL: "admin@localhost.company" DJANGO_SU_EMAIL: "admin@localhost.company"
DJANGO_SU_PASSWORD: "12qwaszx" DJANGO_SU_PASSWORD: "12qwaszx"
API_ABOUT_PAGE: "localhost:8080/api/v1/server/about" API_ABOUT_PAGE: "localhost:8080/api/server/about"
run: | 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 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" 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 - name: End-to-end testing
run: | run: |

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

1
.gitignore vendored

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

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

@ -32,5 +32,10 @@
"name": "cvat", "name": "cvat",
"database": "${workspaceFolder:cvat}/db.sqlite3" "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/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## \[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 ## \[1.7.0] - 2021-11-15
### Added ### 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>) - 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>) - 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>) - 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>) - 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>) 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>) - 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 ### Changed
- UI tracking has been reworked (<https://github.com/openvinotoolkit/cvat/pull/3571>) - 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>) - 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>) - 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>) - 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 # Install requirements
RUN python3 -m venv /opt/venv RUN python3 -m venv /opt/venv
ENV PATH="/opt/venv/bin:${PATH}" 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/ COPY cvat/requirements/ /tmp/requirements/
RUN DATUMARO_HEADLESS=1 python3 -m pip install --no-cache-dir -r /tmp/requirements/${DJANGO_CONFIGURATION}.txt 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} WORKDIR ${HOME}
RUN mkdir data share media keys logs /tmp/supervisord RUN mkdir data share media keys logs /tmp/supervisord
RUN python3 manage.py collectstatic
EXPOSE 8080 EXPOSE 8080
ENTRYPOINT ["/usr/bin/supervisord"] ENTRYPOINT ["/usr/bin/supervisord"]

@ -37,6 +37,5 @@ RUN npm run build:cvat-ui
FROM nginx:mainline-alpine FROM nginx:mainline-alpine
# Replace default.conf configuration to remove unnecessary rules # 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 cvat-ui/react_nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=cvat-ui /tmp/cvat-ui/dist /usr/share/nginx/html/ COPY --from=cvat-ui /tmp/cvat-ui/dist /usr/share/nginx/html/

@ -1,6 +1,6 @@
MIT License 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), 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 for determining if your use of FFmpeg requires any
additional licenses. Intel is not responsible for obtaining any additional licenses. Intel is not responsible for obtaining any
such licenses, nor liable for any licensing fees due in 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 | | [VGGFace2](https://github.com/ox-vgg/vgg_face2) | X | X |
| [Market-1501](https://www.aitribune.com/dataset/2018051063) | X | X | | [Market-1501](https://www.aitribune.com/dataset/2018051063) | X | X |
| [ICDAR13/15](https://rrc.cvc.uab.es/?ch=2) | 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--> <!--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 | | | [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 | | | [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 | | | [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 | | [SiamMask](/serverless/pytorch/foolwood/siammask/nuclio) | tracker | PyTorch | X | X |
| [f-BRS](/serverless/pytorch/saic-vul/fbrs/nuclio) | interactor | PyTorch | X | | | [f-BRS](/serverless/pytorch/saic-vul/fbrs/nuclio) | interactor | PyTorch | X | |
| [HRNet](/serverless/pytorch/saic-vul/hrnet/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 | | [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 | | [Mask RCNN](/serverless/tensorflow/matterport/mask_rcnn/nuclio) | detector | TensorFlow | X | X |
| [RetinaNet](serverless/pytorch/facebookresearch/detectron2/retinanet/nuclio) | detector | PyTorch | 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--> <!--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 such licenses, nor liable for any licensing fees due in
connection with your use of FFmpeg. 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 ## Questions
CVAT usage related questions or unclear concepts can be posted in our 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) - [Intel Software: Computer Vision Annotation Tool: A Universal Approach to Data Annotation](https://software.intel.com/en-us/articles/computer-vision-annotation-tool-a-universal-approach-to-data-annotation)
- [VentureBeat: Intel open-sources CVAT, a toolkit for data labeling](https://venturebeat.com/2019/03/05/intel-open-sources-cvat-a-toolkit-for-data-labeling/) - [VentureBeat: Intel open-sources CVAT, a toolkit for data labeling](https://venturebeat.com/2019/03/05/intel-open-sources-cvat-a-toolkit-for-data-labeling/)
## Projects using CVAT
- [Onepanel](https://github.com/onepanelio/core) 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 --> <!-- prettier-ignore-start -->
<!-- Badges --> <!-- Badges -->

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

@ -1,9 +1,13 @@
# Copyright (C) 2021 Intel Corporation
#
# SPDX-License-Identifier: MIT
#/usr/bin/env python #/usr/bin/env python
import os
import argparse import argparse
import requests
import json import json
from time import sleep
import requests
def import_resources(host, port, cfg_file): def import_resources(host, port, cfg_file):
with open(cfg_file, 'r') as f: with open(cfg_file, 'r') as f:
@ -27,6 +31,19 @@ def import_saved_object(host, port, _type, _id, data):
headers={'kbn-xsrf': 'true'}) headers={'kbn-xsrf': 'true'})
request.raise_for_status() 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__': if __name__ == '__main__':
parser = argparse.ArgumentParser(description='import Kibana 6.x resources', parser = argparse.ArgumentParser(description='import Kibana 6.x resources',
formatter_class=argparse.ArgumentDefaultsHelpFormatter) formatter_class=argparse.ArgumentDefaultsHelpFormatter)
@ -37,4 +54,8 @@ if __name__ == '__main__':
parser.add_argument('-H', '--host', metavar='HOST', default='kibana', parser.add_argument('-H', '--host', metavar='HOST', default='kibana',
help='host of Kibana instance') help='host of Kibana instance')
args = parser.parse_args() 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: cvat:
environment: environment:
CVAT_SERVERLESS: 1 CVAT_SERVERLESS: 1
no_proxy: kibana,logstash,nuclio,${no_proxy}
volumes: volumes:
cvat_events: cvat_events:

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

@ -1,21 +1,35 @@
{ {
"name": "cvat-canvas", "name": "cvat-canvas",
"version": "2.8.0", "version": "2.13.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cvat-canvas", "name": "cvat-canvas",
"version": "2.8.0", "version": "2.13.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/polylabel": "^1.0.5",
"polylabel": "^1.1.0",
"svg.draggable.js": "2.2.2", "svg.draggable.js": "2.2.2",
"svg.draw.js": "^2.0.4", "svg.draw.js": "^2.0.4",
"svg.js": "2.7.1", "svg.js": "2.7.1",
"svg.resize.js": "1.4.3", "svg.resize.js": "1.4.3",
"svg.select.js": "3.0.1" "svg.select.js": "3.0.1"
}, }
"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": { "node_modules/svg.draggable.js": {
"version": "2.2.2", "version": "2.2.2",
@ -77,9 +91,27 @@
"engines": { "engines": {
"node": ">= 0.8.0" "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": { "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": { "svg.draggable.js": {
"version": "2.2.2", "version": "2.2.2",
"resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz", "resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",
@ -127,6 +159,11 @@
"requires": { "requires": {
"svg.js": "^2.6.5" "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", "name": "cvat-canvas",
"version": "2.8.0", "version": "2.13.1",
"description": "Part of Computer Vision Annotation Tool which presents its canvas library", "description": "Part of Computer Vision Annotation Tool which presents its canvas library",
"main": "src/canvas.ts", "main": "src/canvas.ts",
"scripts": { "scripts": {
@ -15,8 +15,9 @@
"not IE 11", "not IE 11",
"> 2%" "> 2%"
], ],
"devDependencies": {},
"dependencies": { "dependencies": {
"@types/polylabel": "^1.0.5",
"polylabel": "^1.1.0",
"svg.draggable.js": "2.2.2", "svg.draggable.js": "2.2.2",
"svg.draw.js": "^2.0.4", "svg.draw.js": "^2.0.4",
"svg.js": "2.7.1", "svg.js": "2.7.1",

@ -37,7 +37,6 @@ polyline.cvat_shape_drawing_opacity {
.cvat_canvas_text { .cvat_canvas_text {
font-weight: bold; font-weight: bold;
font-size: 1.2em;
fill: white; fill: white;
cursor: default; cursor: default;
font-family: Calibri, Candara, Segoe, 'Segoe UI', Optima, Arial, sans-serif; font-family: Calibri, Candara, Segoe, 'Segoe UI', Optima, Arial, sans-serif;
@ -47,7 +46,6 @@ polyline.cvat_shape_drawing_opacity {
} }
.cvat_canvas_text_description { .cvat_canvas_text_description {
font-size: 14px;
fill: yellow; fill: yellow;
font-style: oblique 40deg; font-style: oblique 40deg;
} }
@ -76,7 +74,6 @@ polyline.cvat_shape_drawing_opacity {
} }
.cvat_canvas_issue_region { .cvat_canvas_issue_region {
display: none;
stroke-width: 0; stroke-width: 0;
} }
@ -137,6 +134,10 @@ polyline.cvat_canvas_shape_splitting {
stroke-dasharray: 5; stroke-dasharray: 5;
} }
.svg_select_points_rot {
fill: white;
}
.cvat_canvas_shape .svg_select_points, .cvat_canvas_shape .svg_select_points,
.cvat_canvas_shape .cvat_canvas_cuboid_projections { .cvat_canvas_shape .cvat_canvas_cuboid_projections {
stroke-dasharray: none; stroke-dasharray: none;
@ -166,8 +167,9 @@ polyline.cvat_canvas_shape_splitting {
.cvat_canvas_removable_interaction_point { .cvat_canvas_removable_interaction_point {
cursor: cursor:
url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAxMCAxMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEgMUw5IDlNMSA5TDkgMSIgc3Ryb2tlPSJibGFjayIvPgo8L3N2Zz4K') url(
10 10, 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAxMCAxMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEgMUw5IDlNMSA5TDkgMSIgc3Ryb2tlPSJibGFjayIvPgo8L3N2Zz4K'
) 10 10,
auto; 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 { #cvat_canvas_wrapper {
width: calc(100% - 10px); width: calc(100% - 10px);
height: calc(100% - 10px); height: calc(100% - 10px);
@ -268,6 +281,8 @@ polyline.cvat_canvas_shape_splitting {
} }
#cvat_canvas_bitmap { #cvat_canvas_bitmap {
@extend .cvat_canvas_pixelized;
pointer-events: none; pointer-events: none;
position: absolute; position: absolute;
z-index: 4; z-index: 4;

@ -237,7 +237,8 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
const currentClientID = this.currentShape.node.dataset.originClientId; const currentClientID = this.currentShape.node.dataset.originClientId;
const shapes = Array.from(this.frameContent.getElementsByClassName('cvat_canvas_shape')).filter( 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 const transformedShapes = shapes
.map((shape: HTMLElement): TransformedShape | null => { .map((shape: HTMLElement): TransformedShape | null => {
@ -252,6 +253,10 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
let points = ''; let points = '';
if (shape.tagName === 'polyline' || shape.tagName === 'polygon') { if (shape.tagName === 'polyline' || shape.tagName === 'polygon') {
points = shape.getAttribute('points'); 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') { } else if (shape.tagName === 'rect') {
const x = +shape.getAttribute('x'); const x = +shape.getAttribute('x');
const y = +shape.getAttribute('y'); 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 // SPDX-License-Identifier: MIT
@ -29,7 +29,7 @@ const CanvasVersion = pjson.version;
interface Canvas { interface Canvas {
html(): HTMLDivElement; html(): HTMLDivElement;
setup(frameData: any, objectStates: any[], zLayer?: number): void; setup(frameData: any, objectStates: any[], zLayer?: number): void;
setupIssueRegions(issueRegions: Record<number, number[]>): void; setupIssueRegions(issueRegions: Record<number, { hidden: boolean; points: number[] }>): void;
activate(clientID: number | null, attributeID?: number): void; activate(clientID: number | null, attributeID?: number): void;
rotate(rotationAngle: number): void; rotate(rotationAngle: number): void;
focus(clientID: number, padding?: number): void; focus(clientID: number, padding?: number): void;
@ -53,6 +53,7 @@ interface Canvas {
cancel(): void; cancel(): void;
configure(configuration: Configuration): void; configure(configuration: Configuration): void;
isAbleToChangeFrame(): boolean; isAbleToChangeFrame(): boolean;
destroy(): void;
readonly geometry: Geometry; readonly geometry: Geometry;
} }
@ -76,7 +77,7 @@ class CanvasImpl implements Canvas {
this.model.setup(frameData, objectStates, zLayer); 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); this.model.setupIssueRegions(issueRegions);
} }
@ -163,6 +164,10 @@ class CanvasImpl implements Canvas {
public get geometry(): Geometry { public get geometry(): Geometry {
return this.model.geometry; return this.model.geometry;
} }
public destroy(): void {
this.model.destroy();
}
} }
export type InteractionData = _InteractionData; export type InteractionData = _InteractionData;

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

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

@ -1,7 +1,8 @@
// Copyright (C) 2019-2021 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import polylabel from 'polylabel';
import * as SVG from 'svg.js'; import * as SVG from 'svg.js';
import 'svg.draggable.js'; import 'svg.draggable.js';
@ -24,6 +25,7 @@ import {
translateToSVG, translateToSVG,
translateFromSVG, translateFromSVG,
translateToCanvas, translateToCanvas,
translateFromCanvas,
pointsToNumberArray, pointsToNumberArray,
parsePoints, parsePoints,
displayShapeSize, displayShapeSize,
@ -31,6 +33,8 @@ import {
vectorLength, vectorLength,
ShapeSizeElement, ShapeSizeElement,
DrawnState, DrawnState,
rotate2DPoints,
readPointsFromShape,
} from './shared'; } from './shared';
import { import {
CanvasModel, CanvasModel,
@ -85,7 +89,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
private interactionHandler: InteractionHandler; private interactionHandler: InteractionHandler;
private activeElement: ActiveElement; private activeElement: ActiveElement;
private configuration: Configuration; private configuration: Configuration;
private serviceFlags: { private snapToAngleResize: number;
private innerObjectsFlags: {
drawHidden: Record<number, boolean>; drawHidden: Record<number, boolean>;
}; };
@ -109,7 +114,40 @@ export class CanvasViewImpl implements CanvasView, Listener {
private translateFromCanvas(points: number[]): number[] { private translateFromCanvas(points: number[]): number[] {
const { offset } = this.controller.geometry; 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 { private stringifyToCanvas(points: number[]): string {
@ -122,12 +160,12 @@ export class CanvasViewImpl implements CanvasView, Listener {
}, ''); }, '');
} }
private isServiceHidden(clientID: number): boolean { private isInnerHidden(clientID: number): boolean {
return this.serviceFlags.drawHidden[clientID] || false; return this.innerObjectsFlags.drawHidden[clientID] || false;
} }
private setupServiceHidden(clientID: number, value: boolean): void { private setupInnerFlags(clientID: number, path: 'drawHidden', value: boolean): void {
this.serviceFlags.drawHidden[clientID] = value; this.innerObjectsFlags[path][clientID] = value;
const shape = this.svgShapes[clientID]; const shape = this.svgShapes[clientID];
const text = this.svgTexts[clientID]; const text = this.svgTexts[clientID];
const state = this.drawnStates[clientID]; const state = this.drawnStates[clientID];
@ -143,7 +181,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
text.addClass('cvat_canvas_hidden'); text.addClass('cvat_canvas_hidden');
} }
} else { } else {
delete this.serviceFlags.drawHidden[clientID]; delete this.innerObjectsFlags[path][clientID];
if (state) { if (state) {
if (!state.outside && !state.hidden) { 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 { private onDrawDone(data: any | null, duration: number, continueDraw?: boolean): void {
const hiddenBecauseOfDraw = Object.keys(this.serviceFlags.drawHidden).map((_clientID): number => +_clientID); const hiddenBecauseOfDraw = Object.keys(this.innerObjectsFlags.drawHidden)
.map((_clientID): number => +_clientID);
if (hiddenBecauseOfDraw.length) { if (hiddenBecauseOfDraw.length) {
for (const hidden of hiddenBecauseOfDraw) { 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) { if (state && points) {
const event: CustomEvent = new CustomEvent('canvas.edited', { const event: CustomEvent = new CustomEvent('canvas.edited', {
bubbles: false, bubbles: false,
@ -264,6 +303,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
detail: { detail: {
state, state,
points, points,
rotation: typeof rotation === 'number' ? rotation : state.rotation,
}, },
}); });
@ -388,7 +428,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
private onFindObject(e: MouseEvent): void { private onFindObject(e: MouseEvent): void {
if (e.which === 1 || e.which === 0) { if (e.button === 0) {
const { offset } = this.controller.geometry; const { offset } = this.controller.geometry;
const [x, y] = translateToSVG(this.content, [e.clientX, e.clientY]); const [x, y] = translateToSVG(this.content, [e.clientX, e.clientY]);
const event: CustomEvent = new CustomEvent('canvas.find', { 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`); this.gridPath.setAttribute('stroke-width', `${consts.BASE_GRID_WIDTH / this.geometry.scale}px`);
// Transform all shape points // 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('stroke-width', `${consts.POINTS_STROKE_WIDTH / this.geometry.scale}`);
element.setAttribute('r', `${consts.BASE_POINT_SIZE / 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)) { for (const issueRegion of Object.keys(this.drawnIssueRegions)) {
if (!(issueRegion in issueRegions) || !+issueRegion) { if (!(issueRegion in issueRegions) || !+issueRegion) {
this.drawnIssueRegions[+issueRegion].remove(); this.drawnIssueRegions[+issueRegion].remove();
@ -573,7 +616,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
for (const issueRegion of Object.keys(issueRegions)) { for (const issueRegion of Object.keys(issueRegions)) {
if (issueRegion in this.drawnIssueRegions) continue; if (issueRegion in this.drawnIssueRegions) continue;
const points = this.translateToCanvas(issueRegions[+issueRegion]); const points = this.translateToCanvas(issueRegions[+issueRegion].points);
if (points.length === 2) { if (points.length === 2) {
this.drawnIssueRegions[+issueRegion] = this.adoptedContent this.drawnIssueRegions[+issueRegion] = this.adoptedContent
.circle((consts.BASE_POINT_SIZE * 3 * 2) / this.geometry.scale) .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}`, '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(); this.deactivate();
} }
for (const state of deleted) { this.deleteObjects(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.addObjects(created); this.addObjects(created);
this.updateObjects(updated); this.updateObjects(updated);
this.sortObjects(); this.sortObjects();
@ -744,12 +782,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (e.button !== 0) return; if (e.button !== 0) return;
e.preventDefault(); e.preventDefault();
const pointID = Array.prototype.indexOf.call(
((e.target as HTMLElement).parentElement as HTMLElement).children,
e.target,
);
if (this.activeElement.clientID !== null) { 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( const [state] = this.controller.objects.filter(
(_state: any): boolean => _state.clientID === this.activeElement.clientID, (_state: any): boolean => _state.clientID === this.activeElement.clientID,
); );
@ -821,13 +858,12 @@ export class CanvasViewImpl implements CanvasView, Listener {
e.preventDefault(); e.preventDefault();
}; };
const getGeometry = (): Geometry => this.geometry;
if (value) { if (value) {
const getGeometry = (): Geometry => this.geometry;
(shape as any).selectize(value, { (shape as any).selectize(value, {
deepSelect: true, deepSelect: true,
pointSize: (2 * consts.BASE_POINT_SIZE) / this.geometry.scale, 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 { pointType(cx: number, cy: number): SVG.Circle {
const circle: SVG.Circle = this.nested const circle: SVG.Circle = this.nested
.circle(this.options.pointSize) .circle(this.options.pointSize)
@ -874,8 +910,45 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (handler && handler.nested) { if (handler && handler.nested) {
handler.nested.fill(shape.attr('fill')); 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) { public constructor(model: CanvasModel & Master, controller: CanvasController) {
this.controller = controller; this.controller = controller;
this.geometry = controller.geometry; this.geometry = controller.geometry;
@ -889,7 +962,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
}; };
this.configuration = model.configuration; this.configuration = model.configuration;
this.mode = Mode.IDLE; this.mode = Mode.IDLE;
this.serviceFlags = { this.snapToAngleResize = consts.SNAP_TO_ANGLE_RESIZE_DEFAULT;
this.innerObjectsFlags = {
drawHidden: {}, drawHidden: {},
}; };
@ -1046,11 +1120,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
}); });
window.document.addEventListener('mouseup', (event): void => { window.document.addEventListener('mouseup', this.onMouseUp);
if (event.which === 1 || event.which === 2) { window.document.addEventListener('keydown', this.onShiftKeyDown);
this.controller.disableDrag(); window.document.addEventListener('keyup', this.onShiftKeyUp);
}
});
this.content.addEventListener('wheel', (event): void => { this.content.addEventListener('wheel', (event): void => {
if (event.ctrlKey) return; if (event.ctrlKey) return;
@ -1096,15 +1168,15 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (reason === UpdateReasons.CONFIG_UPDATED) { if (reason === UpdateReasons.CONFIG_UPDATED) {
const { activeElement } = this; const { activeElement } = this;
this.deactivate(); this.deactivate();
const { configuration } = model;
if (model.configuration.displayAllText && !this.configuration.displayAllText) { if (configuration.displayAllText && !this.configuration.displayAllText) {
for (const i in this.drawnStates) { for (const i in this.drawnStates) {
if (!(i in this.svgTexts)) { if (!(i in this.svgTexts)) {
this.svgTexts[i] = this.addText(this.drawnStates[i]); 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) { for (const i in this.drawnStates) {
if (i in this.svgTexts && Number.parseInt(i, 10) !== activeElement.clientID) { if (i in this.svgTexts && Number.parseInt(i, 10) !== activeElement.clientID) {
this.svgTexts[i].remove(); 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.activate(activeElement);
this.editHandler.configurate(this.configuration); this.editHandler.configurate(this.configuration);
this.drawHandler.configurate(this.configuration); this.drawHandler.configurate(this.configuration);
@ -1162,8 +1267,15 @@ export class CanvasViewImpl implements CanvasView, Listener {
} else if (reason === UpdateReasons.FITTED_CANVAS) { } else if (reason === UpdateReasons.FITTED_CANVAS) {
// Canvas geometry is going to be changed. Old object positions aren't valid any more // Canvas geometry is going to be changed. Old object positions aren't valid any more
this.setupObjects([]); this.setupObjects([]);
this.setupIssueRegions({});
this.moveCanvas(); this.moveCanvas();
this.resizeCanvas(); this.resizeCanvas();
this.canvas.dispatchEvent(
new CustomEvent('canvas.reshape', {
bubbles: false,
cancelable: true,
}),
);
} else if ([UpdateReasons.IMAGE_ZOOMED, UpdateReasons.IMAGE_FITTED].includes(reason)) { } else if ([UpdateReasons.IMAGE_ZOOMED, UpdateReasons.IMAGE_FITTED].includes(reason)) {
this.moveCanvas(); this.moveCanvas();
this.transformCanvas(); this.transformCanvas();
@ -1258,7 +1370,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.canvas.style.cursor = 'crosshair'; this.canvas.style.cursor = 'crosshair';
this.mode = Mode.DRAW; this.mode = Mode.DRAW;
if (typeof data.redraw === 'number') { if (typeof data.redraw === 'number') {
this.setupServiceHidden(data.redraw, true); this.setupInnerFlags(data.redraw, 'drawHidden', true);
} }
this.drawHandler.draw(data, this.geometry); this.drawHandler.draw(data, this.geometry);
} else { } else {
@ -1358,6 +1470,18 @@ export class CanvasViewImpl implements CanvasView, Listener {
}, },
}); });
this.canvas.dispatchEvent(event); 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)) { 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 states = this.controller.objects;
const ctx = this.bitmap.getContext('2d'); const ctx = this.bitmap.getContext('2d');
ctx.imageSmoothingEnabled = false;
if (ctx) { if (ctx) {
ctx.fillStyle = 'black'; ctx.fillStyle = 'black';
ctx.fillRect(0, 0, width, height); ctx.fillRect(0, 0, width, height);
@ -1384,31 +1509,34 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (state.hidden || state.outside) continue; if (state.hidden || state.outside) continue;
ctx.fillStyle = 'white'; ctx.fillStyle = 'white';
if (['rectangle', 'polygon', 'cuboid'].includes(state.shapeType)) { if (['rectangle', 'polygon', 'cuboid'].includes(state.shapeType)) {
let points = []; let points = [...state.points];
if (state.shapeType === 'rectangle') { if (state.shapeType === 'rectangle') {
points = [ points = rotate2DPoints(
state.points[0], // xtl points[0] + (points[2] - points[0]) / 2,
state.points[1], // ytl points[1] + (points[3] - points[1]) / 2,
state.points[2], // xbr state.rotation,
state.points[1], // ytl [
state.points[2], // xbr points[0], // xtl
state.points[3], // ybr points[1], // ytl
state.points[0], // xtl points[2], // xbr
state.points[3], // ybr points[1], // ytl
]; points[2], // xbr
points[3], // ybr
points[0], // xtl
points[3], // ybr
],
);
} else if (state.shapeType === 'cuboid') { } else if (state.shapeType === 'cuboid') {
points = [ points = [
state.points[0], points[0],
state.points[1], points[1],
state.points[4], points[4],
state.points[5], points[5],
state.points[8], points[8],
state.points[9], points[9],
state.points[12], points[12],
state.points[13], points[13],
]; ];
} else {
points = [...state.points];
} }
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(points[0], points[1]); ctx.moveTo(points[0], points[1]);
@ -1419,6 +1547,14 @@ export class CanvasViewImpl implements CanvasView, Listener {
ctx.fill(); 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') { if (state.shapeType === 'cuboid') {
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
const points = [ const points = [
@ -1454,6 +1590,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
lock: state.lock, lock: state.lock,
shapeType: state.shapeType, shapeType: state.shapeType,
points: [...state.points], points: [...state.points],
rotation: state.rotation,
attributes: { ...state.attributes }, attributes: { ...state.attributes },
descriptions: [...state.descriptions], descriptions: [...state.descriptions],
zOrder: state.zOrder, zOrder: state.zOrder,
@ -1470,7 +1607,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
const drawnState = this.drawnStates[clientID]; const drawnState = this.drawnStates[clientID];
const shape = this.svgShapes[state.clientID]; const shape = this.svgShapes[state.clientID];
const text = this.svgTexts[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 (drawnState.hidden !== state.hidden || drawnState.outside !== state.outside) {
if (isInvisible) { if (isInvisible) {
@ -1513,6 +1650,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.activate(activeElement); this.activate(activeElement);
} }
if (drawnState.rotation) {
// need to rotate it back before changing points
shape.untransform();
}
if ( if (
state.points.length !== drawnState.points.length || state.points.length !== drawnState.points.length ||
state.points.some((p: number, id: number): boolean => p !== drawnState.points[id]) 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, width: xbr - xtl,
height: ybr - ytl, 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 { } else {
const stringified = this.stringifyToCanvas(translatedPoints); const stringified = this.stringifyToCanvas(translatedPoints);
if (state.shapeType !== 'cuboid') { 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 stateDescriptions = state.descriptions;
const drawnStateDescriptions = drawnState.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 { private addObjects(states: any[]): void {
const { displayAllText } = this.configuration; const { displayAllText } = this.configuration;
for (const state of states) { for (const state of states) {
@ -1592,6 +1761,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.svgShapes[state.clientID] = this.addPolyline(stringified, state); this.svgShapes[state.clientID] = this.addPolyline(stringified, state);
} else if (state.shapeType === 'points') { } else if (state.shapeType === 'points') {
this.svgShapes[state.clientID] = this.addPoints(stringified, state); 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') { } else if (state.shapeType === 'cuboid') {
this.svgShapes[state.clientID] = this.addCuboid(stringified, state); this.svgShapes[state.clientID] = this.addCuboid(stringified, state);
} else { } else {
@ -1785,24 +1956,35 @@ export class CanvasViewImpl implements CanvasView, Listener {
.on('dragstart', (): void => { .on('dragstart', (): void => {
this.mode = Mode.DRAG; this.mode = Mode.DRAG;
hideText(); 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 => { .on('dragend', (e: CustomEvent): void => {
showText(); (shape as any).off('remove.drag');
this.mode = Mode.IDLE; this.mode = Mode.IDLE;
showText();
const p1 = e.detail.handler.startPoints.point; const p1 = e.detail.handler.startPoints.point;
const p2 = e.detail.p; const p2 = e.detail.p;
const delta = 1; const delta = 1;
const { offset } = this.controller.geometry;
const dx2 = (p1.x - p2.x) ** 2; const dx2 = (p1.x - p2.x) ** 2;
const dy2 = (p1.y - p2.y) ** 2; const dy2 = (p1.y - p2.y) ** 2;
if (Math.sqrt(dx2 + dy2) >= delta) { if (Math.sqrt(dx2 + dy2) >= delta) {
const points = pointsToNumberArray( // these points does not take into account possible transformations, applied on the element
shape.attr('points') || `${shape.attr('x')},${shape.attr('y')} ` + // so, if any (like rotation) we need to map them to canvas coordinate space
`${shape.attr('x') + shape.attr('width')},` + let points = readPointsFromShape(shape);
`${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[state.clientID].points = points; const { rotation } = shape.transform();
if (rotation) {
points = this.translatePointsFromRotatedShape(shape, points);
}
points = this.translateFromCanvas(points);
this.canvas.dispatchEvent( this.canvas.dispatchEvent(
new CustomEvent('canvas.dragshape', { new CustomEvent('canvas.dragshape', {
bubbles: false, bubbles: false,
@ -1837,18 +2019,33 @@ export class CanvasViewImpl implements CanvasView, Listener {
let shapeSizeElement: ShapeSizeElement | null = null; let shapeSizeElement: ShapeSizeElement | null = null;
let resized = false; let resized = false;
const resizeFinally = (): void => {
if (shapeSizeElement) {
shapeSizeElement.rm();
shapeSizeElement = null;
}
this.mode = Mode.IDLE;
};
(shape as any) (shape as any)
.resize({ .resize({
snapToGrid: 0.1, snapToGrid: 0.1,
snapToAngle: this.snapToAngleResize,
}) })
.on('resizestart', (): void => { .on('resizestart', (): void => {
this.mode = Mode.RESIZE; this.mode = Mode.RESIZE;
resized = false; resized = false;
hideDirection(); hideDirection();
hideText(); hideText();
if (state.shapeType === 'rectangle') { if (state.shapeType === 'rectangle' || state.shapeType === 'ellipse') {
shapeSizeElement = displayShapeSize(this.adoptedContent, this.adoptedText); 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 => { .on('resizing', (): void => {
resized = true; resized = true;
@ -1857,25 +2054,29 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
}) })
.on('resizedone', (): void => { .on('resizedone', (): void => {
if (shapeSizeElement) { (shape as any).off('remove.resize');
shapeSizeElement.rm(); resizeFinally();
}
showDirection(); showDirection();
showText(); 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) { // these points does not take into account possible transformations, applied on the element
const { offset } = this.controller.geometry; // so, if any (like rotation) we need to map them to canvas coordinate space
let points = readPointsFromShape(shape);
const points = pointsToNumberArray( // let's keep current points, but they could be rewritten in updateObjects
shape.attr('points') || `${shape.attr('x')},${shape.attr('y')} ` + this.drawnStates[clientID].points = this.translateFromCanvas(points);
`${shape.attr('x') + shape.attr('width')},` + this.drawnStates[clientID].rotation = rotation;
`${shape.attr('y') + shape.attr('height')}`, if (rotation) {
).map((x: number): number => x - offset); points = this.translatePointsFromRotatedShape(shape, points);
}
this.drawnStates[state.clientID].points = points; // points = this.translateFromCanvas(points);
this.canvas.dispatchEvent( this.canvas.dispatchEvent(
new CustomEvent('canvas.resizeshape', { new CustomEvent('canvas.resizeshape', {
bubbles: false, 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. // Update text position after corresponding box has been moved, resized, etc.
private updateTextPosition(text: SVG.Text, shape: SVG.Shape): void { private updateTextPosition(text: SVG.Text, shape: SVG.Shape): void {
if (text.node.style.display === 'none') return; // wrong transformation matrix if (text.node.style.display === 'none') return; // wrong transformation matrix
let box = (shape.node as any).getBBox(); const { textFontSize, textPosition } = this.configuration;
// 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,
]);
box = { text.untransform();
x: Math.min(x1, x2), text.style({ 'font-size': `${textFontSize}px` });
y: Math.min(y1, y2), const { rotation } = shape.transform();
width: Math.max(x1, x2) - Math.min(x1, x2),
height: Math.max(y1, y2) - Math.min(y1, y2),
};
// Find the best place for a text // Find the best place for a text
let [clientX, clientY]: number[] = [box.x + box.width, box.y]; let [clientX, clientY, clientCX, clientCY]: number[] = [0, 0, 0, 0];
if ( if (textPosition === 'center') {
clientX + ((text.node as any) as SVGTextElement) let cx = 0;
.getBBox().width + consts.TEXT_MARGIN > this.canvas.offsetWidth let cy = 0;
) { if (shape.type === 'rect') {
[clientX, clientY] = [box.x, box.y]; // for rectangle finding a center is simple
} cx = +shape.attr('x') + +shape.attr('width') / 2;
cy = +shape.attr('y') + +shape.attr('height') / 2;
// Translate back to text SVG } else if (shape.type === 'ellipse') {
const [x, y]: number[] = translateToSVG(this.text, [ // even simpler for ellipses
clientX + consts.TEXT_MARGIN, cx = +shape.attr('cx');
clientY + consts.TEXT_MARGIN, 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 // 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) { for (const tspan of (text.lines() as any).members) {
tspan.attr('x', text.attr('x')); tspan.attr('x', text.attr('x'));
} }
@ -1969,6 +2217,14 @@ export class CanvasViewImpl implements CanvasView, Listener {
private addText(state: any): SVG.Text { private addText(state: any): SVG.Text {
const { undefinedAttrValue } = this.configuration; 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 { const {
label, clientID, attributes, source, descriptions, label, clientID, attributes, source, descriptions,
} = state; } = state;
@ -1979,29 +2235,36 @@ export class CanvasViewImpl implements CanvasView, Listener {
return this.adoptedText return this.adoptedText
.text((block): void => { .text((block): void => {
block.tspan(`${label.name} ${clientID} (${source})`).style('text-transform', 'uppercase'); block.tspan(`${withLabel ? label.name : ''} ${withID ? clientID : ''} ${withSource ? `(${source})` : ''}`).style({
for (const desc of descriptions) { 'text-transform': 'uppercase',
block });
.tspan(`${desc}`) if (withDescriptions) {
.attr({ for (const desc of descriptions) {
dy: '1em', block
x: 0, .tspan(`${desc}`)
}) .attr({
.addClass('cvat_canvas_text_description'); dy: '1em',
x: 0,
})
.addClass('cvat_canvas_text_description');
}
} }
for (const attrID of Object.keys(attributes)) { if (withAttr) {
const value = attributes[attrID] === undefinedAttrValue ? '' : attributes[attrID]; for (const attrID of Object.keys(attributes)) {
block const value = attributes[attrID] === undefinedAttrValue ? '' : attributes[attrID];
.tspan(`${attrNames[attrID]}: ${value}`) block
.attr({ .tspan(`${attrNames[attrID]}: ${value}`)
attrID, .attr({
dy: '1em', attrID,
x: 0, dy: '1em',
}) x: 0,
.addClass('cvat_canvas_text_attribute'); })
.addClass('cvat_canvas_text_attribute');
}
} }
}) })
.move(0, 0) .move(0, 0)
.style({ 'font-size': textFontSize })
.addClass('cvat_canvas_text'); .addClass('cvat_canvas_text');
} }
@ -2023,11 +2286,15 @@ export class CanvasViewImpl implements CanvasView, Listener {
.move(xtl, ytl) .move(xtl, ytl)
.addClass('cvat_canvas_shape'); .addClass('cvat_canvas_shape');
if (state.rotation) {
rect.rotate(state.rotation);
}
if (state.occluded) { if (state.occluded) {
rect.addClass('cvat_canvas_shape_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'); rect.addClass('cvat_canvas_hidden');
} }
@ -2053,7 +2320,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
polygon.addClass('cvat_canvas_shape_occluded'); 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'); polygon.addClass('cvat_canvas_hidden');
} }
@ -2079,7 +2346,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
polyline.addClass('cvat_canvas_shape_occluded'); 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'); polyline.addClass('cvat_canvas_hidden');
} }
@ -2106,7 +2373,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
cube.addClass('cvat_canvas_shape_occluded'); 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'); cube.addClass('cvat_canvas_hidden');
} }
@ -2139,6 +2406,39 @@ export class CanvasViewImpl implements CanvasView, Listener {
return group; 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 { private addPoints(points: string, state: any): SVG.PolyLine {
const shape = this.adoptedContent const shape = this.adoptedContent
.polyline(points) .polyline(points)
@ -2155,7 +2455,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
const group = this.setupPoints(shape, state); 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'); 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 ' + const ARROW_PATH = 'M13.162 6.284L.682.524a.483.483 0 0 0-.574.134.477.477 0 ' +
'0 0-.012.59L4.2 6.72.096 12.192a.479.479 0 0 0 .585.724l12.48-5.76a.48.48 0 0 0 0-.872z'; '0 0-.012.59L4.2 6.72.096 12.192a.479.479 0 0 0 .585.724l12.48-5.76a.48.48 0 0 0 0-.872z';
const BASE_PATTERN_SIZE = 5; 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 { export default {
BASE_STROKE_WIDTH, BASE_STROKE_WIDTH,
@ -33,4 +37,8 @@ export default {
UNDEFINED_ATTRIBUTE_VALUE, UNDEFINED_ATTRIBUTE_VALUE,
ARROW_PATH, ARROW_PATH,
BASE_PATTERN_SIZE, 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', 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 { export function intersection(p1: Point, p2: Point, p3: Point, p4: Point): Point | null {
const L1 = line(p1, p2); // Check if none of the lines are of length 0
const L2 = line(p3, p4); const { x: x1, y: y1 } = p1;
const { x: x2, y: y2 } = p2;
const D = L1[0] * L2[1] - L1[1] * L2[0]; const { x: x3, y: y3 } = p3;
const Dx = L1[2] * L2[1] - L1[1] * L2[2]; const { x: x4, y: y4 } = p4;
const Dy = L1[0] * L2[2] - L1[2] * L2[0];
if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) {
let x = null; return null;
let y = null; }
if (Math.abs(D) > Number.EPSILON) {
x = Dx / D; const denominator = ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1));
y = Dy / D;
return { x, y }; // 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 { export class Equation {

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

@ -17,38 +17,25 @@ export interface InteractionHandler {
transform(geometry: Geometry): void; transform(geometry: Geometry): void;
interact(interactData: InteractionData): void; interact(interactData: InteractionData): void;
configurate(config: Configuration): void; configurate(config: Configuration): void;
destroy(): void;
cancel(): void; cancel(): void;
} }
export class InteractionHandlerImpl implements InteractionHandler { export class InteractionHandlerImpl implements InteractionHandler {
private onInteraction: (shapes: InteractionResult[] | null, shapesUpdated?: boolean, isDone?: boolean) => void; private onInteraction: (shapes: InteractionResult[] | null, shapesUpdated?: boolean, isDone?: boolean) => void;
private configuration: Configuration; private configuration: Configuration;
private geometry: Geometry; private geometry: Geometry;
private canvas: SVG.Container; private canvas: SVG.Container;
private interactionData: InteractionData; private interactionData: InteractionData;
private cursorPosition: { x: number; y: number }; private cursorPosition: { x: number; y: number };
private shapesWereUpdated: boolean; private shapesWereUpdated: boolean;
private interactionShapes: SVG.Shape[]; private interactionShapes: SVG.Shape[];
private currentInteractionShape: SVG.Shape | null; private currentInteractionShape: SVG.Shape | null;
private crosshair: Crosshair; private crosshair: Crosshair;
private threshold: SVG.Rect | null; private threshold: SVG.Rect | null;
private thresholdRectSize: number; private thresholdRectSize: number;
private intermediateShape: PropType<InteractionData, 'intermediateShape'>; private intermediateShape: PropType<InteractionData, 'intermediateShape'>;
private drawnIntermediateShape: SVG.Shape; private drawnIntermediateShape: SVG.Shape;
private thresholdWasModified: boolean; private thresholdWasModified: boolean;
private prepareResult(): InteractionResult[] { private prepareResult(): InteractionResult[] {
@ -375,6 +362,27 @@ export class InteractionHandlerImpl implements InteractionHandler {
return false; 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( public constructor(
onInteraction: ( onInteraction: (
shapes: InteractionResult[] | null, shapes: InteractionResult[] | null,
@ -452,26 +460,8 @@ export class InteractionHandlerImpl implements InteractionHandler {
} }
}); });
window.addEventListener('keyup', (e: KeyboardEvent): void => { window.document.addEventListener('keyup', this.onKeyUp);
if (this.interactionData.enabled && e.keyCode === 17) { window.document.addEventListener('keydown', this.onKeyDown);
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;
}
});
} }
public transform(geometry: Geometry): void { public transform(geometry: Geometry): void {
@ -552,4 +542,9 @@ export class InteractionHandlerImpl implements InteractionHandler {
this.release(); this.release();
this.onInteraction(null); 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 // SPDX-License-Identifier: MIT
@ -44,6 +44,7 @@ export interface DrawnState {
source: 'AUTO' | 'MANUAL'; source: 'AUTO' | 'MANUAL';
shapeType: string; shapeType: string;
points?: number[]; points?: number[];
rotation: number;
attributes: Record<number, string>; attributes: Record<number, string>;
descriptions: string[]; descriptions: string[];
zOrder?: number; zOrder?: number;
@ -95,16 +96,30 @@ export function displayShapeSize(shapesContainer: SVG.Container, textContainer:
.fill('white') .fill('white')
.addClass('cvat_canvas_text'), .addClass('cvat_canvas_text'),
update(shape: SVG.Shape): void { update(shape: SVG.Shape): void {
const bbox = shape.bbox(); let text = `${Math.round(shape.width())}x${Math.round(shape.height())}px`;
const text = `${bbox.width.toFixed(1)}x${bbox.height.toFixed(1)}`; if (shape.type === 'rect' || shape.type === 'ellipse') {
const [x, y]: number[] = translateToSVG( 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, (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 this.sizeElement
.clear() .clear()
.plain(text) .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 { rm(): void {
if (this.sizeElement) { if (this.sizeElement) {
@ -117,6 +132,23 @@ export function displayShapeSize(shapesContainer: SVG.Container, textContainer:
return shapeSize; 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[] { export function pointsToNumberArray(points: string | Point[]): number[] {
if (Array.isArray(points)) { if (Array.isArray(points)) {
return points.reduce((acc: number[], point: Point): number[] => { 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 { export function stringifyPoints(points: (Point | number)[]): string {
if (typeof points[0] === 'number') { if (typeof points[0] === 'number') {
return points.reduce((acc: string, val: number, idx: number): string => { return points.reduce((acc: string, val: number, idx: number): string => {
@ -187,4 +235,8 @@ export function translateToCanvas(offset: number, points: number[]): number[] {
return points.map((coord: number): number => coord + offset); 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]; 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 = this.remember('_resizeHandler');
handler.resize = function (e: any) { handler.resize = function (e: any) {
const { event } = e.detail; const { event } = e.detail;
this.rotationPointPressed = e.type === 'rot';
if ( if (
event.button === 0 && event.button === 0 &&
// ignore shift key for cuboid change perspective // ignore shift key for cuboids (change perspective) and rectangles (precise rotation)
(!event.shiftKey || this.el.parent().hasClass('cvat_canvas_shape_cuboid')) && (!event.shiftKey || (
!event.altKey this.el.parent().hasClass('cvat_canvas_shape_cuboid')
|| this.el.type === 'rect')
) && !event.altKey
) { ) {
return handler.constructor.prototype.resize.call(this, e); return handler.constructor.prototype.resize.call(this, e);
} }
}; };
handler.update = function (e: any) { handler.update = function (e: any) {
this.m = this.el.node.getScreenCTM().inverse(); if (!this.rotationPointPressed) {
return handler.constructor.prototype.update.call(this, e); this.m = this.el.node.getScreenCTM().inverse();
}
handler.constructor.prototype.update.call(this, e);
}; };
} else { } else {
originalResize.call(this, ...args); originalResize.call(this, ...args);

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

@ -126,6 +126,7 @@ export interface Canvas3dModel {
configureShapes(shapeProperties: any): void; configureShapes(shapeProperties: any): void;
fit(): void; fit(): void;
group(groupData: GroupData): void; group(groupData: GroupData): void;
destroy(): void;
} }
export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel { export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
@ -234,8 +235,8 @@ export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
} }
public isAbleToChangeFrame(): boolean { public isAbleToChangeFrame(): boolean {
const isUnable = [Mode.DRAG, Mode.EDIT, Mode.RESIZE, Mode.INTERACT, Mode.BUSY].includes(this.data.mode) 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'); (this.data.mode === Mode.DRAW && typeof this.data.drawData.redraw === 'number');
return !isUnable; return !isUnable;
} }
@ -340,4 +341,6 @@ export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
public get groupData(): GroupData { public get groupData(): GroupData {
return { ...this.data.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), (_state: any): boolean => _state.clientID === Number(intersects[0].object.name),
); );
if (item.length !== 0) { if (item.length !== 0) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
this.model.data.groupData.grouped = this.model.data.groupData.grouped.filter( this.model.data.groupData.grouped = this.model.data.groupData.grouped.filter(
(_state: any): boolean => _state.clientID !== Number(intersects[0].object.name), (_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.screenInit = { x: diffX, y: diffY };
this.action.rotation.screenMove = { x: diffX, y: diffY }; this.action.rotation.screenMove = { x: diffX, y: diffY };
if ( if (
this.model.data.selected this.model.data.selected &&
&& !this.model.data.selected.perspective.userData.lock !this.model.data.selected.perspective.userData.lock &&
&& !this.model.data.selected.perspective.userData.hidden !this.model.data.selected.perspective.userData.hidden
) { ) {
this.action.scan = view; this.action.scan = view;
this.model.mode = Mode.EDIT; this.model.mode = Mode.EDIT;
@ -698,8 +699,8 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
cuboid.setOpacity(opacity); cuboid.setOpacity(opacity);
if ( if (
this.model.data.activeElement.clientID === clientID this.model.data.activeElement.clientID === clientID &&
&& ![Mode.DRAG_CANVAS, Mode.GROUP].includes(this.mode) ![Mode.DRAG_CANVAS, Mode.GROUP].includes(this.mode)
) { ) {
cuboid.setOpacity(selectedOpacity); cuboid.setOpacity(selectedOpacity);
if (!object.lock) { if (!object.lock) {
@ -964,12 +965,12 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
const sphereCenter = points.geometry.boundingSphere.center; const sphereCenter = points.geometry.boundingSphere.center;
const { radius } = points.geometry.boundingSphere; const { radius } = points.geometry.boundingSphere;
if (!this.views.perspective.camera) return; if (!this.views.perspective.camera) return;
const xRange = -radius / 2 < this.views.perspective.camera.position.x - sphereCenter.x const xRange = -radius / 2 < this.views.perspective.camera.position.x - sphereCenter.x &&
&& 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 const yRange = -radius / 2 < this.views.perspective.camera.position.y - sphereCenter.y &&
&& 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 const zRange = -radius / 2 < this.views.perspective.camera.position.z - sphereCenter.z &&
&& radius / 2 > this.views.perspective.camera.position.z - sphereCenter.z; radius / 2 > this.views.perspective.camera.position.z - sphereCenter.z;
let newX = 0; let newX = 0;
let newY = 0; let newY = 0;
let newZ = 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 { private positionAllViews(x: number, y: number, z: number, animation: boolean): void {
if ( if (
this.views.perspective.controls this.views.perspective.controls &&
&& this.views.top.controls this.views.top.controls &&
&& this.views.side.controls this.views.side.controls &&
&& this.views.front.controls this.views.front.controls
) { ) {
this.views.perspective.controls.setLookAt(x - 8, y - 8, z + 3, x, y, z, animation); 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); 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 { private renderTranslateAction(view: ViewType, viewType: any): void {
if ( if (
this.action.translation.helper.x === this.views[view].rayCaster.mouseVector.x 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.y === this.views[view].rayCaster.mouseVector.y
) { ) {
return; return;
} }
@ -1332,8 +1333,8 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
} }
if ( if (
this.action.resize.recentMouseVector.x === currentPosX this.action.resize.recentMouseVector.x === currentPosX &&
&& this.action.resize.recentMouseVector.y === currentPosY this.action.resize.recentMouseVector.y === currentPosY
) { ) {
return; return;
} }
@ -1736,15 +1737,15 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
y: canvas.offsetTop + canvas.offsetHeight / 2, y: canvas.offsetTop + canvas.offsetHeight / 2,
}; };
if ( if (
this.action.rotation.screenInit.x === this.action.rotation.screenMove.x this.action.rotation.screenInit.x === this.action.rotation.screenMove.x &&
&& this.action.rotation.screenInit.y === this.action.rotation.screenMove.y this.action.rotation.screenInit.y === this.action.rotation.screenMove.y
) { ) {
return; return;
} }
if ( if (
this.action.rotation.recentMouseVector.x === this.views[view].rayCaster.mouseVector.x 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.y === this.views[view].rayCaster.mouseVector.y
) { ) {
return; return;
} }

@ -1,18 +1,18 @@
{ {
"name": "cvat-core", "name": "cvat-core",
"version": "3.17.0", "version": "4.2.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cvat-core", "name": "cvat-core",
"version": "3.17.0", "version": "4.2.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"axios": "^0.21.4", "axios": "^0.21.4",
"browser-or-node": "^1.2.1", "browser-or-node": "^1.2.1",
"cvat-data": "../cvat-data", "cvat-data": "../cvat-data",
"detect-browser": "^5.2.0", "detect-browser": "^5.2.1",
"error-stack-parser": "^2.0.2", "error-stack-parser": "^2.0.2",
"form-data": "^2.5.0", "form-data": "^2.5.0",
"jest-config": "^26.6.3", "jest-config": "^26.6.3",
@ -20,7 +20,8 @@
"json-logic-js": "^2.0.1", "json-logic-js": "^2.0.1",
"platform": "^1.3.5", "platform": "^1.3.5",
"quickhull": "^1.0.3", "quickhull": "^1.0.3",
"store": "^2.0.12" "store": "^2.0.12",
"tus-js-client": "^2.3.0"
}, },
"devDependencies": { "devDependencies": {
"coveralls": "^3.0.5", "coveralls": "^3.0.5",
@ -38,6 +39,12 @@
}, },
"devDependencies": {} "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": { "node_modules/@babel/code-frame": {
"version": "7.15.8", "version": "7.15.8",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", "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", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" "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": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "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", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" "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": { "node_modules/cvat-data": {
"resolved": "../cvat-data", "resolved": "../cvat-data",
"link": true "link": true
@ -2279,9 +2300,9 @@
} }
}, },
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.14.4", "version": "1.14.8",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
"integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==", "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@ -2814,7 +2835,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
}, },
@ -3669,6 +3689,11 @@
"node": ">= 10.13.0" "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": { "node_modules/js-cookie": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", "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", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" "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": { "node_modules/log-driver": {
"version": "1.2.7", "version": "1.2.7",
"resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz",
@ -4630,6 +4709,18 @@
"node": ">= 6" "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": { "node_modules/psl": {
"version": "1.8.0", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
@ -4661,6 +4752,11 @@
"node": ">=0.6" "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": { "node_modules/quickhull": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/quickhull/-/quickhull-1.0.3.tgz", "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", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" "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": { "node_modules/requizzle": {
"version": "0.2.3", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.3.tgz", "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.3.tgz",
@ -4887,6 +4988,14 @@
"node": ">=0.12" "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": { "node_modules/rimraf": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@ -5189,11 +5298,6 @@
"node": ">=0.10.0" "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": { "node_modules/saxes": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
@ -5932,6 +6036,25 @@
"node": "*" "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": { "node_modules/tweetnacl": {
"version": "0.14.5", "version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
@ -6078,6 +6201,15 @@
"integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
"deprecated": "Please see https://github.com/lydell/urix#deprecated" "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": { "node_modules/use": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
@ -6207,7 +6339,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"dependencies": { "dependencies": {
"isexe": "^2.0.0" "isexe": "^2.0.0"
}, },
@ -7581,6 +7712,15 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" "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": { "combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "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": { "cvat-data": {
"version": "file:../cvat-data", "version": "file:../cvat-data",
"requires": { "requires": {
@ -8099,9 +8244,9 @@
} }
}, },
"follow-redirects": { "follow-redirects": {
"version": "1.14.4", "version": "1.14.8",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
"integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==" "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA=="
}, },
"for-in": { "for-in": {
"version": "1.0.2", "version": "1.0.2",
@ -8482,8 +8627,7 @@
"is-stream": { "is-stream": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="
"dev": true
}, },
"is-typedarray": { "is-typedarray": {
"version": "1.0.0", "version": "1.0.0",
@ -9155,6 +9299,11 @@
"supports-color": "^7.0.0" "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": { "js-cookie": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", "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", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" "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": { "log-driver": {
"version": "1.2.7", "version": "1.2.7",
"resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz",
@ -9893,6 +10096,15 @@
"sisteransi": "^1.0.5" "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": { "psl": {
"version": "1.8.0", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
@ -9918,6 +10130,11 @@
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
"dev": true "dev": true
}, },
"querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
},
"quickhull": { "quickhull": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/quickhull/-/quickhull-1.0.3.tgz", "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", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" "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": { "requizzle": {
"version": "0.2.3", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.3.tgz", "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", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
"integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" "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": { "rimraf": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@ -10330,14 +10557,6 @@
"is-number": "^3.0.0", "is-number": "^3.0.0",
"repeat-string": "^1.6.1" "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" "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": { "tweetnacl": {
"version": "0.14.5", "version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "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", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
"integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" "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": { "use": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
@ -11148,7 +11397,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"requires": { "requires": {
"isexe": "^2.0.0" "isexe": "^2.0.0"
} }

@ -1,6 +1,6 @@
{ {
"name": "cvat-core", "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", "description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js", "main": "babel.config.js",
"scripts": { "scripts": {
@ -31,10 +31,11 @@
"error-stack-parser": "^2.0.2", "error-stack-parser": "^2.0.2",
"form-data": "^2.5.0", "form-data": "^2.5.0",
"jest-config": "^26.6.3", "jest-config": "^26.6.3",
"json-logic-js": "^2.0.1",
"js-cookie": "^2.2.0", "js-cookie": "^2.2.0",
"json-logic-js": "^2.0.1",
"platform": "^1.3.5", "platform": "^1.3.5",
"quickhull": "^1.0.3", "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 // SPDX-License-Identifier: MIT
@ -8,11 +8,13 @@
PolygonShape, PolygonShape,
PolylineShape, PolylineShape,
PointsShape, PointsShape,
EllipseShape,
CuboidShape, CuboidShape,
RectangleTrack, RectangleTrack,
PolygonTrack, PolygonTrack,
PolylineTrack, PolylineTrack,
PointsTrack, PointsTrack,
EllipseTrack,
CuboidTrack, CuboidTrack,
Track, Track,
Shape, Shape,
@ -48,6 +50,9 @@
case 'points': case 'points':
shapeModel = new PointsShape(shapeData, clientID, color, injection); shapeModel = new PointsShape(shapeData, clientID, color, injection);
break; break;
case 'ellipse':
shapeModel = new EllipseShape(shapeData, clientID, color, injection);
break;
case 'cuboid': case 'cuboid':
shapeModel = new CuboidShape(shapeData, clientID, color, injection); shapeModel = new CuboidShape(shapeData, clientID, color, injection);
break; break;
@ -77,6 +82,9 @@
case 'points': case 'points':
trackModel = new PointsTrack(trackData, clientID, color, injection); trackModel = new PointsTrack(trackData, clientID, color, injection);
break; break;
case 'ellipse':
trackModel = new EllipseTrack(trackData, clientID, color, injection);
break;
case 'cuboid': case 'cuboid':
trackModel = new CuboidTrack(trackData, clientID, color, injection); trackModel = new CuboidTrack(trackData, clientID, color, injection);
break; break;
@ -235,7 +243,7 @@
const object = this.objects[state.clientID]; const object = this.objects[state.clientID];
if (typeof object === 'undefined') { if (typeof object === 'undefined') {
throw new ArgumentError( throw new ArgumentError(
'The object has not been saved yet. Call ObjectState.put([state]) before you can merge it', 'The object is not in collection yet. Call ObjectState.put([state]) before you can merge it',
); );
} }
return object; return object;
@ -282,6 +290,7 @@
frame: object.frame, frame: object.frame,
points: [...object.points], points: [...object.points],
occluded: object.occluded, occluded: object.occluded,
rotation: object.rotation,
zOrder: object.zOrder, zOrder: object.zOrder,
outside: false, outside: false,
attributes: Object.keys(object.attributes).reduce((accumulator, attrID) => { attributes: Object.keys(object.attributes).reduce((accumulator, attrID) => {
@ -333,6 +342,7 @@
type: shapeType, type: shapeType,
frame: +keyframe, frame: +keyframe,
points: [...shape.points], points: [...shape.points],
rotation: shape.rotation,
occluded: shape.occluded, occluded: shape.occluded,
outside: shape.outside, outside: shape.outside,
zOrder: shape.zOrder, zOrder: shape.zOrder,
@ -442,6 +452,7 @@
const position = { const position = {
type: objectState.shapeType, type: objectState.shapeType,
points: [...objectState.points], points: [...objectState.points],
rotation: objectState.rotation,
occluded: objectState.occluded, occluded: objectState.occluded,
outside: objectState.outside, outside: objectState.outside,
zOrder: objectState.zOrder, zOrder: objectState.zOrder,
@ -481,6 +492,12 @@
return shape; return shape;
}); });
prev.shapes.push(position); 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; prev.shapes[prev.shapes.length - 1].outside = true;
let clientID = ++this.count; let clientID = ++this.count;
@ -606,6 +623,10 @@
shape: 0, shape: 0,
track: 0, track: 0,
}, },
ellipse: {
shape: 0,
track: 0,
},
cuboid: { cuboid: {
shape: 0, shape: 0,
track: 0, track: 0,
@ -725,6 +746,7 @@
checkObjectType('object state', state, null, ObjectState); checkObjectType('object state', state, null, ObjectState);
checkObjectType('state client ID', state.clientID, 'undefined', null); checkObjectType('state client ID', state.clientID, 'undefined', null);
checkObjectType('state frame', state.frame, 'integer', 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 attributes', state.attributes, null, Object);
checkObjectType('state label', state.label, null, Label); checkObjectType('state label', state.label, null, Label);
@ -768,6 +790,7 @@
label_id: state.label.id, label_id: state.label.id,
occluded: state.occluded || false, occluded: state.occluded || false,
points: [...state.points], points: [...state.points],
rotation: state.rotation || 0,
type: state.shapeType, type: state.shapeType,
z_order: state.zOrder, z_order: state.zOrder,
source: state.source, source: state.source,
@ -787,6 +810,7 @@
occluded: state.occluded || false, occluded: state.occluded || false,
outside: false, outside: false,
points: [...state.points], points: [...state.points],
rotation: state.rotation || 0,
type: state.shapeType, type: state.shapeType,
z_order: state.zOrder, z_order: state.zOrder,
}, },
@ -844,7 +868,7 @@
if (typeof object === 'undefined') { if (typeof object === 'undefined') {
throw new ArgumentError('The object has not been saved yet. Call annotations.put([state]) before'); 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)) { if (distance !== null && (minimumDistance === null || distance < minimumDistance)) {
minimumDistance = distance; minimumDistance = distance;
minimumState = state; minimumState = state;
@ -893,35 +917,14 @@
search(filters, frameFrom, frameTo) { search(filters, frameFrom, frameTo) {
const sign = Math.sign(frameTo - frameFrom); const sign = Math.sign(frameTo - frameFrom);
const filtersStr = JSON.stringify(filters); const filtersStr = JSON.stringify(filters);
const containsDifficultProperties = filtersStr.match(/"var":"width"/) || filtersStr.match(/"var":"height"/); const linearSearch = 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;
}
}
return next;
};
const keyframesMemory = {};
const predicate = sign > 0 ? (frame) => frame <= frameTo : (frame) => frame >= frameTo; const predicate = sign > 0 ? (frame) => frame <= frameTo : (frame) => frame >= frameTo;
const update = sign > 0 ? (frame) => frame + 1 : (frame) => frame - 1; const update = sign > 0 ? (frame) => frame + 1 : (frame) => frame - 1;
for (let frame = frameFrom; predicate(frame); frame = update(frame)) { for (let frame = frameFrom; predicate(frame); frame = update(frame)) {
// First prepare all data for the frame // First prepare all data for the frame
// Consider all shapes, tags, and not outside tracks that have keyframe here // Consider all shapes, tags, and not outside tracks that have keyframe here
// 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( const statesData = [].concat(
(frame in this.shapes ? this.shapes[frame] : []) (frame in this.shapes ? this.shapes[frame] : [])
.filter((shape) => !shape.removed) .filter((shape) => !shape.removed)
@ -931,7 +934,9 @@
.map((tag) => tag.get(frame)), .map((tag) => tag.get(frame)),
); );
const tracks = Object.values(this.tracks) const tracks = Object.values(this.tracks)
.filter((track) => frame in track.shapes || frame === frameFrom || frame === frameTo) .filter((track) => (
frame in track.shapes || frame === frameFrom ||
frame === frameTo || linearSearch))
.filter((track) => !track.removed); .filter((track) => !track.removed);
statesData.push(...tracks.map((track) => track.get(frame)).filter((state) => !state.outside)); statesData.push(...tracks.map((track) => track.get(frame)).filter((state) => !state.outside));
@ -942,31 +947,6 @@
// Filtering // Filtering
const filtered = this.annotationsFilter.filter(statesData, filters); 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) { if (filtered.length) {
return frame; return frame;
} }

@ -47,13 +47,28 @@
} }
} else if (shapeType === ObjectShape.CUBOID) { } else if (shapeType === ObjectShape.CUBOID) {
if (points.length / 2 !== 8) { 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 { } else {
throw new ArgumentError(`Unknown value of shapeType has been received ${shapeType}`); 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) { function checkShapeArea(shapeType, points) {
const MIN_SHAPE_LENGTH = 3; const MIN_SHAPE_LENGTH = 3;
const MIN_SHAPE_AREA = 9; const MIN_SHAPE_AREA = 9;
@ -62,6 +77,12 @@
return true; 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 xmin = Number.MAX_SAFE_INTEGER;
let xmax = Number.MIN_SAFE_INTEGER; let xmax = Number.MIN_SAFE_INTEGER;
let ymin = Number.MAX_SAFE_INTEGER; let ymin = Number.MAX_SAFE_INTEGER;
@ -76,7 +97,6 @@
if (shapeType === ObjectShape.POLYLINE) { if (shapeType === ObjectShape.POLYLINE) {
const length = Math.max(xmax - xmin, ymax - ymin); const length = Math.max(xmax - xmin, ymax - ymin);
return length >= MIN_SHAPE_LENGTH; return length >= MIN_SHAPE_LENGTH;
} }
@ -84,30 +104,34 @@
return area >= MIN_SHAPE_AREA; return area >= MIN_SHAPE_AREA;
} }
function fitPoints(shapeType, points, maxX, maxY) { function rotatePoint(x, y, angle, cx = 0, cy = 0) {
const fittedPoints = []; const sin = Math.sin((angle * Math.PI) / 180);
const cos = Math.cos((angle * Math.PI) / 180);
for (let i = 0; i < points.length - 1; i += 2) { const rotX = (x - cx) * cos - (y - cy) * sin + cx;
const x = points[i]; const rotY = (y - cy) * cos + (x - cx) * sin + cy;
const y = points[i + 1]; return [rotX, rotY];
}
checkObjectType('coordinate', x, 'number', null); function fitPoints(shapeType, points, rotation, maxX, maxY) {
checkObjectType('coordinate', y, 'number', null); 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) { for (let i = 0; i < points.length - 1; i += 2) {
const [x, y] = points.slice(i); const x = points[i];
inside = inside || (x >= 0 && x <= width && y >= 0 && y <= height); 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) { function validateAttributeValue(value, attr) {
@ -345,13 +369,13 @@
checkNumberOfPoints(this.shapeType, data.points); checkNumberOfPoints(this.shapeType, data.points);
// cut points // cut points
const { width, height, filename } = this.frameMeta[frame]; 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; let check = true;
if (filename && filename.slice(filename.length - 3) === 'pcd') { if (filename && filename.slice(filename.length - 3) === 'pcd') {
check = false; check = false;
} }
if (check) { if (check) {
if (!checkShapeArea(this.shapeType, fittedPoints) || checkOutside(fittedPoints, width, height)) { if (!checkShapeArea(this.shapeType, fittedPoints)) {
fittedPoints = []; fittedPoints = [];
} }
} }
@ -492,6 +516,7 @@
constructor(data, clientID, color, injection) { constructor(data, clientID, color, injection) {
super(data, clientID, color, injection); super(data, clientID, color, injection);
this.points = data.points; this.points = data.points;
this.rotation = data.rotation || 0;
this.occluded = data.occluded; this.occluded = data.occluded;
this.zOrder = data.z_order; this.zOrder = data.z_order;
} }
@ -504,6 +529,7 @@
occluded: this.occluded, occluded: this.occluded,
z_order: this.zOrder, z_order: this.zOrder,
points: [...this.points], points: [...this.points],
rotation: this.rotation,
attributes: Object.keys(this.attributes).reduce((attributeAccumulator, attrId) => { attributes: Object.keys(this.attributes).reduce((attributeAccumulator, attrId) => {
attributeAccumulator.push({ attributeAccumulator.push({
spec_id: attrId, spec_id: attrId,
@ -535,6 +561,7 @@
lock: this.lock, lock: this.lock,
zOrder: this.zOrder, zOrder: this.zOrder,
points: [...this.points], points: [...this.points],
rotation: this.rotation,
attributes: { ...this.attributes }, attributes: { ...this.attributes },
descriptions: [...this.descriptions], descriptions: [...this.descriptions],
label: this.label, label: this.label,
@ -548,9 +575,11 @@
}; };
} }
_savePoints(points, frame) { _savePoints(points, rotation, frame) {
const undoPoints = this.points; const undoPoints = this.points;
const undoRotation = this.rotation;
const redoPoints = points; const redoPoints = points;
const redoRotation = rotation;
const undoSource = this.source; const undoSource = this.source;
const redoSource = Source.MANUAL; const redoSource = Source.MANUAL;
@ -559,11 +588,13 @@
() => { () => {
this.points = undoPoints; this.points = undoPoints;
this.source = undoSource; this.source = undoSource;
this.rotation = undoRotation;
this.updated = Date.now(); this.updated = Date.now();
}, },
() => { () => {
this.points = redoPoints; this.points = redoPoints;
this.source = redoSource; this.source = redoSource;
this.rotation = redoRotation;
this.updated = Date.now(); this.updated = Date.now();
}, },
[this.clientID], [this.clientID],
@ -572,6 +603,7 @@
this.source = Source.MANUAL; this.source = Source.MANUAL;
this.points = points; this.points = points;
this.rotation = rotation;
} }
_saveOccluded(occluded, frame) { _saveOccluded(occluded, frame) {
@ -637,6 +669,7 @@
const updated = data.updateFlags; const updated = data.updateFlags;
const fittedPoints = this._validateStateBeforeSave(frame, data, updated); const fittedPoints = this._validateStateBeforeSave(frame, data, updated);
const { rotation } = data;
// Now when all fields are validated, we can apply them // Now when all fields are validated, we can apply them
if (updated.label) { if (updated.label) {
@ -652,7 +685,7 @@
} }
if (updated.points && fittedPoints.length) { if (updated.points && fittedPoints.length) {
this._savePoints(fittedPoints, frame); this._savePoints(fittedPoints, rotation, frame);
} }
if (updated.occluded) { if (updated.occluded) {
@ -696,6 +729,7 @@
zOrder: value.z_order, zOrder: value.z_order,
points: value.points, points: value.points,
outside: value.outside, outside: value.outside,
rotation: value.rotation || 0,
attributes: value.attributes.reduce((attributeAccumulator, attr) => { attributes: value.attributes.reduce((attributeAccumulator, attr) => {
attributeAccumulator[attr.spec_id] = attr.value; attributeAccumulator[attr.spec_id] = attr.value;
return attributeAccumulator; return attributeAccumulator;
@ -736,6 +770,7 @@
occluded: this.shapes[frame].occluded, occluded: this.shapes[frame].occluded,
z_order: this.shapes[frame].zOrder, z_order: this.shapes[frame].zOrder,
points: [...this.shapes[frame].points], points: [...this.shapes[frame].points],
rotation: this.shapes[frame].rotation,
outside: this.shapes[frame].outside, outside: this.shapes[frame].outside,
attributes: Object.keys(this.shapes[frame].attributes).reduce( attributes: Object.keys(this.shapes[frame].attributes).reduce(
(attributeAccumulator, attrId) => { (attributeAccumulator, attrId) => {
@ -1009,22 +1044,21 @@
); );
} }
_savePoints(points, frame) { _savePoints(points, rotation, frame) {
const current = this.get(frame); const current = this.get(frame);
const wasKeyframe = frame in this.shapes; const wasKeyframe = frame in this.shapes;
const undoSource = this.source; const undoSource = this.source;
const redoSource = Source.MANUAL; const redoSource = Source.MANUAL;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined; const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = wasKeyframe ? const redoShape = wasKeyframe ? { ...this.shapes[frame], points, rotation } : {
{ ...this.shapes[frame], points } : frame,
{ points,
frame, rotation,
points, zOrder: current.zOrder,
zOrder: current.zOrder, outside: current.outside,
outside: current.outside, occluded: current.occluded,
occluded: current.occluded, attributes: {},
attributes: {}, };
};
this.shapes[frame] = redoShape; this.shapes[frame] = redoShape;
this.source = Source.MANUAL; this.source = Source.MANUAL;
@ -1049,6 +1083,7 @@
{ {
frame, frame,
outside, outside,
rotation: current.rotation,
zOrder: current.zOrder, zOrder: current.zOrder,
points: current.points, points: current.points,
occluded: current.occluded, occluded: current.occluded,
@ -1078,6 +1113,7 @@
{ {
frame, frame,
occluded, occluded,
rotation: current.rotation,
zOrder: current.zOrder, zOrder: current.zOrder,
points: current.points, points: current.points,
outside: current.outside, outside: current.outside,
@ -1107,6 +1143,7 @@
{ {
frame, frame,
zOrder, zOrder,
rotation: current.rotation,
occluded: current.occluded, occluded: current.occluded,
points: current.points, points: current.points,
outside: current.outside, outside: current.outside,
@ -1139,6 +1176,7 @@
const redoShape = keyframe ? const redoShape = keyframe ?
{ {
frame, frame,
rotation: current.rotation,
zOrder: current.zOrder, zOrder: current.zOrder,
points: current.points, points: current.points,
outside: current.outside, outside: current.outside,
@ -1172,6 +1210,7 @@
const updated = data.updateFlags; const updated = data.updateFlags;
const fittedPoints = this._validateStateBeforeSave(frame, data, updated); const fittedPoints = this._validateStateBeforeSave(frame, data, updated);
const { rotation } = data;
if (updated.label) { if (updated.label) {
this._saveLabel(data.label, frame); this._saveLabel(data.label, frame);
@ -1194,7 +1233,7 @@
} }
if (updated.points && fittedPoints.length) { if (updated.points && fittedPoints.length) {
this._savePoints(fittedPoints, frame); this._savePoints(fittedPoints, rotation, frame);
} }
if (updated.outside) { if (updated.outside) {
@ -1246,6 +1285,7 @@
if (leftPosition) { if (leftPosition) {
return { return {
points: [...leftPosition.points], points: [...leftPosition.points],
rotation: leftPosition.rotation,
occluded: leftPosition.occluded, occluded: leftPosition.occluded,
outside: leftPosition.outside, outside: leftPosition.outside,
zOrder: leftPosition.zOrder, zOrder: leftPosition.zOrder,
@ -1256,10 +1296,11 @@
if (rightPosition) { if (rightPosition) {
return { return {
points: [...rightPosition.points], points: [...rightPosition.points],
rotation: rightPosition.rotation,
occluded: rightPosition.occluded, occluded: rightPosition.occluded,
outside: true,
zOrder: rightPosition.zOrder, zOrder: rightPosition.zOrder,
keyframe: targetFrame in this.shapes, keyframe: targetFrame in this.shapes,
outside: true,
}; };
} }
@ -1356,20 +1397,84 @@
checkNumberOfPoints(this.shapeType, this.points); checkNumberOfPoints(this.shapeType, this.points);
} }
static distance(points, x, y) { static distance(points, x, y, angle) {
const [xtl, ytl, xbr, ybr] = points; 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 // Cursor is outside of a box
return null; return null;
} }
// The shortest distance from point to an edge // 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 { class PolygonShape extends PolyShape {
constructor(data, clientID, color, injection) { constructor(data, clientID, color, injection) {
@ -1509,6 +1614,7 @@
class CuboidShape extends Shape { class CuboidShape extends Shape {
constructor(data, clientID, color, injection) { constructor(data, clientID, color, injection) {
super(data, clientID, color, injection); super(data, clientID, color, injection);
this.rotation = 0;
this.shapeType = ObjectShape.CUBOID; this.shapeType = ObjectShape.CUBOID;
this.pinned = false; this.pinned = false;
checkNumberOfPoints(this.shapeType, this.points); 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) { interpolatePosition(leftPosition, rightPosition, offset) {
const positionOffset = leftPosition.points.map((point, index) => rightPosition.points[index] - point); const positionOffset = leftPosition.points.map((point, index) => rightPosition.points[index] - point);
return { return {
points: leftPosition.points.map((point, index) => point + positionOffset[index] * offset), points: leftPosition.points.map((point, index) => point + positionOffset[index] * offset),
rotation:
(leftPosition.rotation + findAngleDiff(
rightPosition.rotation, leftPosition.rotation,
) * offset + 360) % 360,
occluded: leftPosition.occluded, occluded: leftPosition.occluded,
outside: leftPosition.outside, outside: leftPosition.outside,
zOrder: leftPosition.zOrder, zOrder: leftPosition.zOrder,
@ -1650,10 +1785,18 @@
} }
class PolyTrack extends Track { 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) { interpolatePosition(leftPosition, rightPosition, offset) {
if (offset === 0) { if (offset === 0) {
return { return {
points: [...leftPosition.points], points: [...leftPosition.points],
rotation: leftPosition.rotation,
occluded: leftPosition.occluded, occluded: leftPosition.occluded,
outside: leftPosition.outside, outside: leftPosition.outside,
zOrder: leftPosition.zOrder, zOrder: leftPosition.zOrder,
@ -1900,6 +2043,7 @@
return { return {
points: toArray(reducedPoints), points: toArray(reducedPoints),
rotation: leftPosition.rotation,
occluded: leftPosition.occluded, occluded: leftPosition.occluded,
outside: leftPosition.outside, outside: leftPosition.outside,
zOrder: leftPosition.zOrder, zOrder: leftPosition.zOrder,
@ -1962,6 +2106,7 @@
points: leftPosition.points.map( points: leftPosition.points.map(
(value, index) => value + (rightPosition.points[index] - value) * offset, (value, index) => value + (rightPosition.points[index] - value) * offset,
), ),
rotation: leftPosition.rotation,
occluded: leftPosition.occluded, occluded: leftPosition.occluded,
outside: leftPosition.outside, outside: leftPosition.outside,
zOrder: leftPosition.zOrder, zOrder: leftPosition.zOrder,
@ -1970,6 +2115,7 @@
return { return {
points: [...leftPosition.points], points: [...leftPosition.points],
rotation: leftPosition.rotation,
occluded: leftPosition.occluded, occluded: leftPosition.occluded,
outside: leftPosition.outside, outside: leftPosition.outside,
zOrder: leftPosition.zOrder, zOrder: leftPosition.zOrder,
@ -1984,6 +2130,7 @@
this.pinned = false; this.pinned = false;
for (const shape of Object.values(this.shapes)) { for (const shape of Object.values(this.shapes)) {
checkNumberOfPoints(this.shapeType, shape.points); checkNumberOfPoints(this.shapeType, shape.points);
shape.rotation = 0; // is not supported
} }
} }
@ -1992,6 +2139,7 @@
return { return {
points: leftPosition.points.map((point, index) => point + positionOffset[index] * offset), points: leftPosition.points.map((point, index) => point + positionOffset[index] * offset),
rotation: leftPosition.rotation,
occluded: leftPosition.occluded, occluded: leftPosition.occluded,
outside: leftPosition.outside, outside: leftPosition.outside,
zOrder: leftPosition.zOrder, zOrder: leftPosition.zOrder,
@ -2003,6 +2151,7 @@
PolygonTrack.distance = PolygonShape.distance; PolygonTrack.distance = PolygonShape.distance;
PolylineTrack.distance = PolylineShape.distance; PolylineTrack.distance = PolylineShape.distance;
PointsTrack.distance = PointsShape.distance; PointsTrack.distance = PointsShape.distance;
EllipseTrack.distance = EllipseShape.distance;
CuboidTrack.distance = CuboidShape.distance; CuboidTrack.distance = CuboidShape.distance;
module.exports = { module.exports = {
@ -2010,11 +2159,13 @@
PolygonShape, PolygonShape,
PolylineShape, PolylineShape,
PointsShape, PointsShape,
EllipseShape,
CuboidShape, CuboidShape,
RectangleTrack, RectangleTrack,
PolygonTrack, PolygonTrack,
PolylineTrack, PolylineTrack,
PointsTrack, PointsTrack,
EllipseTrack,
CuboidTrack, CuboidTrack,
Track, Track,
Shape, Shape,

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

@ -276,7 +276,7 @@
if (instance instanceof Task) { if (instance instanceof Task) {
result = await serverProxy.tasks.exportDataset(instance.id, format, name, saveImages); result = await serverProxy.tasks.exportDataset(instance.id, format, name, saveImages);
} else if (instance instanceof Job) { } 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 { } else {
result = await serverProxy.projects.exportDataset(instance.id, format, name, saveImages); result = await serverProxy.projects.exportDataset(instance.id, format, name, saveImages);
} }
@ -284,6 +284,22 @@
return result; 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) { function undoActions(session, count) {
const sessionType = session instanceof Task ? 'task' : 'job'; const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType); const cache = getCache(sessionType);
@ -366,6 +382,7 @@
importAnnotations, importAnnotations,
exportAnnotations, exportAnnotations,
exportDataset, exportDataset,
importDataset,
undoActions, undoActions,
redoActions, redoActions,
freezeHistory, freezeHistory,

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

@ -1,4 +1,4 @@
// Copyright (C) 2019-2021 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -15,7 +15,6 @@ function build() {
const Statistics = require('./statistics'); const Statistics = require('./statistics');
const Comment = require('./comment'); const Comment = require('./comment');
const Issue = require('./issue'); const Issue = require('./issue');
const Review = require('./review');
const { Job, Task } = require('./session'); const { Job, Task } = require('./session');
const { Project } = require('./project'); const { Project } = require('./project');
const implementProject = require('./project-implementation'); const implementProject = require('./project-implementation');
@ -23,6 +22,7 @@ function build() {
const MLModel = require('./ml-model'); const MLModel = require('./ml-model');
const { FrameData } = require('./frames'); const { FrameData } = require('./frames');
const { CloudStorage } = require('./cloud-storage'); const { CloudStorage } = require('./cloud-storage');
const Organization = require('./organization');
const enums = require('./enums'); const enums = require('./enums');
@ -697,6 +697,9 @@ function build() {
* @property {string} proxy Axios proxy settings. * @property {string} proxy Axios proxy settings.
* For more details please read <a href="https://github.com/axios/axios"> here </a> * For more details please read <a href="https://github.com/axios/axios"> here </a>
* @memberof module:API.cvat.config * @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 * @memberof module:API.cvat.config
*/ */
get backendAPI() { get backendAPI() {
@ -711,6 +714,18 @@ function build() {
set proxy(value) { set proxy(value) {
config.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 * Namespace contains some library information e.g. api version
@ -758,7 +773,7 @@ function build() {
/** /**
* @typedef {Object} CloudStorageFilter * @typedef {Object} CloudStorageFilter
* @property {string} displayName Check if displayName contains this value * @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 {module:API.cvat.enums.ProviderType} providerType Check if providerType equal this value
* @property {integer} id Check if id equals this value * @property {integer} id Check if id equals this value
* @property {integer} page Get specific page * @property {integer} page Get specific page
@ -784,6 +799,50 @@ function build() {
return result; 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 is used for access to classes
* @namespace classes * @namespace classes
@ -802,9 +861,9 @@ function build() {
MLModel, MLModel,
Comment, Comment,
Issue, Issue,
Review,
FrameData, FrameData,
CloudStorage, CloudStorage,
Organization,
}, },
}; };
@ -818,6 +877,7 @@ function build() {
cvat.client = Object.freeze(cvat.client); cvat.client = Object.freeze(cvat.client);
cvat.enums = Object.freeze(cvat.enums); cvat.enums = Object.freeze(cvat.enums);
cvat.cloudStorages = Object.freeze(cvat.cloudStorages); cvat.cloudStorages = Object.freeze(cvat.cloudStorages);
cvat.organizations = Object.freeze(cvat.organizations);
const implementAPI = require('./api-implementation'); const implementAPI = require('./api-implementation');

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

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

@ -1,4 +1,4 @@
// Copyright (C) 2019-2021 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -36,7 +36,7 @@
if (!(prop in fields)) { if (!(prop in fields)) {
throw new ArgumentError(`Unsupported filter property has been received: "${prop}"`); throw new ArgumentError(`Unsupported filter property has been received: "${prop}"`);
} else if (!fields[prop](filter[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 { class FieldUpdateTrigger {
constructor(initialFields) { constructor() {
const data = { ...initialFields }; let updatedFlags = {};
Object.defineProperties( Object.defineProperties(
this, this,
Object.freeze({ Object.freeze({
...Object.assign(
{},
...Array.from(Object.keys(data), (key) => ({
[key]: {
get: () => data[key],
set: (value) => {
data[key] = value;
},
enumerable: true,
},
})),
),
reset: { reset: {
value: () => { value: () => {
Object.keys(data).forEach((key) => { updatedFlags = {};
data[key] = false; },
}); },
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, isString,
checkFilter, checkFilter,
checkObjectType, checkObjectType,
negativeIDGenerator,
checkExclusiveFields, checkExclusiveFields,
camelToSnake, camelToSnake,
FieldUpdateTrigger, FieldUpdateTrigger,

@ -1,8 +1,11 @@
// Copyright (C) 2019-2021 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
module.exports = { module.exports = {
backendAPI: '/api/v1', backendAPI: '/api',
proxy: false, 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 // SPDX-License-Identifier: MIT

@ -34,33 +34,51 @@
}); });
/** /**
* Task dimension * Job stages
* @enum * @enum {string}
* @name DimensionType * @name JobStage
* @memberof module:API.cvat.enums * @memberof module:API.cvat.enums
* @property {string} DIMENSION_2D '2d' * @property {string} ANNOTATION 'annotation'
* @property {string} DIMENSION_3D '3d' * @property {string} VALIDATION 'validation'
* @property {string} ACCEPTANCE 'acceptance'
* @readonly * @readonly
*/ */
const DimensionType = Object.freeze({ const JobStage = Object.freeze({
DIMENSION_2D: '2d', ANNOTATION: 'annotation',
DIMENSION_3D: '3d', VALIDATION: 'validation',
ACCEPTANCE: 'acceptance',
}); });
/** /**
* Review statuses * Job states
* @enum {string} * @enum {string}
* @name ReviewStatus * @name JobState
* @memberof module:API.cvat.enums * @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} REJECTED 'rejected'
* @property {string} REVIEW_FURTHER 'review_further'
* @readonly * @readonly
*/ */
const ReviewStatus = Object.freeze({ const JobState = Object.freeze({
ACCEPTED: 'accepted', NEW: 'new',
IN_PROGRESS: 'in progress',
COMPLETED: 'completed',
REJECTED: 'rejected', 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', POLYGON: 'polygon',
POLYLINE: 'polyline', POLYLINE: 'polyline',
POINTS: 'points', POINTS: 'points',
ELLIPSE: 'ellipse',
CUBOID: 'cuboid', CUBOID: 'cuboid',
}); });
@ -340,11 +359,13 @@
* @memberof module:API.cvat.enums * @memberof module:API.cvat.enums
* @property {string} AWS_S3 'AWS_S3_BUCKET' * @property {string} AWS_S3 'AWS_S3_BUCKET'
* @property {string} AZURE 'AZURE_CONTAINER' * @property {string} AZURE 'AZURE_CONTAINER'
* @property {string} GOOGLE_CLOUD_STORAGE 'GOOGLE_CLOUD_STORAGE'
* @readonly * @readonly
*/ */
const CloudStorageProviderType = Object.freeze({ const CloudStorageProviderType = Object.freeze({
AWS_S3_BUCKET: 'AWS_S3_BUCKET', AWS_S3_BUCKET: 'AWS_S3_BUCKET',
AZURE_CONTAINER: 'AZURE_CONTAINER', 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} KEY_SECRET_KEY_PAIR 'KEY_SECRET_KEY_PAIR'
* @property {string} ACCOUNT_NAME_TOKEN_PAIR 'ACCOUNT_NAME_TOKEN_PAIR' * @property {string} ACCOUNT_NAME_TOKEN_PAIR 'ACCOUNT_NAME_TOKEN_PAIR'
* @property {string} ANONYMOUS_ACCESS 'ANONYMOUS_ACCESS' * @property {string} ANONYMOUS_ACCESS 'ANONYMOUS_ACCESS'
* @property {string} KEY_FILE_PATH 'KEY_FILE_PATH'
* @readonly * @readonly
*/ */
const CloudStorageCredentialsType = Object.freeze({ const CloudStorageCredentialsType = Object.freeze({
KEY_SECRET_KEY_PAIR: 'KEY_SECRET_KEY_PAIR', KEY_SECRET_KEY_PAIR: 'KEY_SECRET_KEY_PAIR',
ACCOUNT_NAME_TOKEN_PAIR: 'ACCOUNT_NAME_TOKEN_PAIR', ACCOUNT_NAME_TOKEN_PAIR: 'ACCOUNT_NAME_TOKEN_PAIR',
ANONYMOUS_ACCESS: 'ANONYMOUS_ACCESS', 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 = { module.exports = {
ShareFileType, ShareFileType,
TaskStatus, TaskStatus,
ReviewStatus, JobStage,
JobState,
TaskMode, TaskMode,
AttributeType, AttributeType,
ObjectType, ObjectType,
@ -380,5 +440,7 @@
DimensionType, DimensionType,
CloudStorageProviderType, CloudStorageProviderType,
CloudStorageCredentialsType, CloudStorageCredentialsType,
MembershipRole,
SortingMethod,
}; };
})(); })();

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -23,6 +23,7 @@
height, height,
name, name,
taskID, taskID,
jobID,
frameNumber, frameNumber,
startFrame, startFrame,
stopFrame, stopFrame,
@ -69,6 +70,17 @@
value: taskID, value: taskID,
writable: false, writable: false,
}, },
/**
* @name jid
* @type {integer}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
*/
jid: {
value: jobID,
writable: false,
},
/** /**
* @name number * @name number
* @type {integer} * @type {integer}
@ -191,7 +203,7 @@
const taskDataCache = frameDataCache[this.tid]; const taskDataCache = frameDataCache[this.tid];
const activeChunk = taskDataCache.activeChunkRequest; const activeChunk = taskDataCache.activeChunkRequest;
activeChunk.request = serverProxy.frames activeChunk.request = serverProxy.frames
.getData(this.tid, activeChunk.chunkNumber) .getData(this.tid, this.jid, activeChunk.chunkNumber)
.then((chunk) => { .then((chunk) => {
frameDataCache[this.tid].activeChunkRequest.completed = true; frameDataCache[this.tid].activeChunkRequest.completed = true;
if (!taskDataCache.nextChunkRequest) { if (!taskDataCache.nextChunkRequest) {
@ -366,7 +378,7 @@
} }
class FrameBuffer { class FrameBuffer {
constructor(size, chunkSize, stopFrame, taskID) { constructor(size, chunkSize, stopFrame, taskID, jobID) {
this._size = size; this._size = size;
this._buffer = {}; this._buffer = {};
this._contextImage = {}; this._contextImage = {};
@ -375,6 +387,7 @@
this._stopFrame = stopFrame; this._stopFrame = stopFrame;
this._activeFillBufferRequest = false; this._activeFillBufferRequest = false;
this._taskID = taskID; this._taskID = taskID;
this._jobID = jobID;
} }
isContextImageAvailable(frame) { isContextImageAvailable(frame) {
@ -411,6 +424,7 @@
const frameData = new FrameData({ const frameData = new FrameData({
...frameMeta, ...frameMeta,
taskID: this._taskID, taskID: this._taskID,
jobID: this._jobID,
frameNumber: requestedFrame, frameNumber: requestedFrame,
startFrame: frameDataCache[this._taskID].startFrame, startFrame: frameDataCache[this._taskID].startFrame,
stopFrame: frameDataCache[this._taskID].stopFrame, stopFrame: frameDataCache[this._taskID].stopFrame,
@ -463,31 +477,47 @@
let bufferedFrames = new Set(); 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 // Need to decode chunks in sequence
// eslint-disable-next-line no-async-promise-executor // eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
for (const chunkIdx in this._requestedChunks) { for (const chunkIdx of Object.keys(this._requestedChunks)) {
if (Object.prototype.hasOwnProperty.call(this._requestedChunks, chunkIdx)) { try {
try { const chunkFrames = await this.requestOneChunkFrames(chunkIdx);
const chunkFrames = await this.requestOneChunkFrames(chunkIdx); if (chunkIdx in this._requestedChunks) {
if (chunkIdx in this._requestedChunks) { bufferedFrames = new Set([...bufferedFrames, ...chunkFrames]);
bufferedFrames = new Set([...bufferedFrames, ...chunkFrames]);
this._buffer = { buffersToBeCommited.push(this._requestedChunks[chunkIdx].buffer);
...this._buffer, delete this._requestedChunks[chunkIdx];
...this._requestedChunks[chunkIdx].buffer, if (Object.keys(this._requestedChunks).length === 0) {
}; commitBuffers();
delete this._requestedChunks[chunkIdx]; resolve(bufferedFrames);
if (Object.keys(this._requestedChunks).length === 0) {
resolve(bufferedFrames);
}
} else {
reject(chunkIdx);
break;
} }
} catch (error) { } else {
reject(error); commitBuffers();
reject(chunkIdx);
break; 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) { 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]; delete this._buffer[frame];
} }
} }
@ -520,6 +550,7 @@
let frame = new FrameData({ let frame = new FrameData({
...frameMeta, ...frameMeta,
taskID, taskID,
jobID,
frameNumber, frameNumber,
startFrame: frameDataCache[taskID].startFrame, startFrame: frameDataCache[taskID].startFrame,
stopFrame: frameDataCache[taskID].stopFrame, stopFrame: frameDataCache[taskID].stopFrame,
@ -548,7 +579,6 @@
} else if (fillBuffer) { } else if (fillBuffer) {
this.clear(); this.clear();
await this.makeFillRequest(frameNumber, frameStep, fillBuffer ? null : 1); await this.makeFillRequest(frameNumber, frameStep, fillBuffer ? null : 1);
frame = this._buffer[frameNumber]; frame = this._buffer[frameNumber];
} else { } else {
this.clear(); this.clear();
@ -576,10 +606,10 @@
} }
} }
async function getImageContext(taskID, frame) { async function getImageContext(jobID, frame) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
serverProxy.frames serverProxy.frames
.getImageContext(taskID, frame) .getImageContext(jobID, frame)
.then((result) => { .then((result) => {
if (isNode) { if (isNode) {
// eslint-disable-next-line no-undef // 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)) { if (frameDataCache[taskID].frameBuffer.isContextImageAvailable(frame)) {
return frameDataCache[taskID].frameBuffer.getContextImage(frame); return frameDataCache[taskID].frameBuffer.getContextImage(frame);
} }
const response = getImageContext(taskID, frame); const response = getImageContext(jobID, frame);
frameDataCache[taskID].frameBuffer.addContextImage(frame, response); frameDataCache[taskID].frameBuffer.addContextImage(frame, response);
return frameDataCache[taskID].frameBuffer.getContextImage(frame); return frameDataCache[taskID].frameBuffer.getContextImage(frame);
} }
async function getPreview(taskID) { async function getPreview(taskID = null, jobID = null) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Just go to server and get preview (no any cache) // Just go to server and get preview (no any cache)
serverProxy.frames serverProxy.frames
.getPreview(taskID) .getPreview(taskID, jobID)
.then((result) => { .then((result) => {
if (isNode) { if (isNode) {
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
@ -632,6 +662,7 @@
async function getFrame( async function getFrame(
taskID, taskID,
jobID,
chunkSize, chunkSize,
chunkType, chunkType,
mode, mode,
@ -674,6 +705,7 @@
chunkSize, chunkSize,
stopFrame, stopFrame,
taskID, taskID,
jobID,
), ),
decodedBlocksCacheSize, decodedBlocksCacheSize,
activeChunkRequest: null, activeChunkRequest: null,
@ -684,7 +716,7 @@
frameDataCache[taskID].provider.setRenderSize(frameMeta.width, frameMeta.height); 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) { function getRanges(taskID) {

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

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

@ -28,6 +28,7 @@ const { Source } = require('./enums');
descriptions: [], descriptions: [],
points: null, points: null,
rotation: null,
outside: null, outside: null,
occluded: null, occluded: null,
keyframe: 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: { group: {
/** /**
* Object with short group info { color, id } * Object with short group info { color, id }
@ -410,6 +433,9 @@ const { Source } = require('./enums');
if (typeof serialized.color === 'string') { if (typeof serialized.color === 'string') {
this.color = serialized.color; this.color = serialized.color;
} }
if (typeof serialized.rotation === 'number') {
this.rotation = serialized.rotation;
}
if (Array.isArray(serialized.points)) { if (Array.isArray(serialized.points)) {
this.points = 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 { getPreview } = require('./frames');
const { Project } = require('./project'); const { Project } = require('./project');
const { exportDataset } = require('./annotations'); const { exportDataset, importDataset } = require('./annotations');
function implementProject(projectClass) { function implementProject(projectClass) {
projectClass.prototype.save.implementation = async function () { projectClass.prototype.save.implementation = async function () {
const trainingProjectCopy = this.trainingProject;
if (typeof this.id !== 'undefined') { if (typeof this.id !== 'undefined') {
// project has been already created, need to update some data const projectData = this._updateTrigger.getUpdated(this, {
const projectData = { bugTracker: 'bug_tracker',
name: this.name, trainingProject: 'training_project',
assignee_id: this.assignee ? this.assignee.id : null, assignee: 'assignee_id',
bug_tracker: this.bugTracker, });
labels: [...this._internalData.labels.map((el) => el.toJSON())], if (projectData.assignee_id) {
}; projectData.assignee_id = projectData.assignee_id.id;
}
if (trainingProjectCopy) { if (projectData.labels) {
projectData.training_project = trainingProjectCopy; projectData.labels = projectData.labels.map((el) => el.toJSON());
} }
await serverProxy.projects.save(this.id, projectData); await serverProxy.projects.save(this.id, projectData);
this._updateTrigger.reset();
return this; return this;
} }
// initial creating // initial creating
const projectSpec = { const projectSpec = {
name: this.name, name: this.name,
labels: [...this.labels.map((el) => el.toJSON())], labels: this.labels.map((el) => el.toJSON()),
}; };
if (this.bugTracker) { if (this.bugTracker) {
projectSpec.bug_tracker = this.bugTracker; projectSpec.bug_tracker = this.bugTracker;
} }
if (trainingProjectCopy) { if (this.trainingProject) {
projectSpec.training_project = trainingProjectCopy; projectSpec.training_project = this.trainingProject;
} }
const project = await serverProxy.projects.create(projectSpec); const project = await serverProxy.projects.create(projectSpec);
@ -61,11 +61,30 @@
}; };
projectClass.prototype.annotations.exportDataset.implementation = async function ( projectClass.prototype.annotations.exportDataset.implementation = async function (
format, saveImages, customName, format,
saveImages,
customName,
) { ) {
const result = exportDataset(this, format, customName, saveImages); const result = exportDataset(this, format, customName, saveImages);
return result; 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; return projectClass;
} }

@ -5,9 +5,9 @@
(() => { (() => {
const PluginRegistry = require('./plugins'); const PluginRegistry = require('./plugins');
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
const { Task } = require('./session');
const { Label } = require('./labels'); const { Label } = require('./labels');
const User = require('./user'); const User = require('./user');
const { FieldUpdateTrigger } = require('./common');
/** /**
* Class representing a project * Class representing a project
@ -37,6 +37,8 @@
dimension: undefined, dimension: undefined,
}; };
const updateTrigger = new FieldUpdateTrigger();
for (const property in data) { for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) { if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
data[property] = initialData[property]; data[property] = initialData[property];
@ -44,7 +46,6 @@
} }
data.labels = []; data.labels = [];
data.tasks = [];
if (Array.isArray(initialData.labels)) { if (Array.isArray(initialData.labels)) {
for (const label of 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') { if (typeof initialData.training_project === 'object') {
data.training_project = { ...initialData.training_project }; data.training_project = { ...initialData.training_project };
} }
@ -97,6 +85,7 @@
throw new ArgumentError('Value must not be empty'); throw new ArgumentError('Value must not be empty');
} }
data.name = value; data.name = value;
updateTrigger.update('name');
}, },
}, },
@ -125,6 +114,7 @@
throw new ArgumentError('Value must be a user instance'); throw new ArgumentError('Value must be a user instance');
} }
data.assignee = assignee; data.assignee = assignee;
updateTrigger.update('assignee');
}, },
}, },
/** /**
@ -149,6 +139,7 @@
get: () => data.bug_tracker, get: () => data.bug_tracker,
set: (tracker) => { set: (tracker) => {
data.bug_tracker = tracker; data.bug_tracker = tracker;
updateTrigger.update('bugTracker');
}, },
}, },
/** /**
@ -210,19 +201,9 @@
}); });
data.labels = [...deletedLabels, ...labels]; 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 * Subsets array for related tasks
* @name subsets * @name subsets
@ -257,11 +238,15 @@
} else { } else {
data.training_project = updatedProject; data.training_project = updatedProject;
} }
updateTrigger.update('trainingProject');
}, },
}, },
_internalData: { _internalData: {
get: () => data, get: () => data,
}, },
_updateTrigger: {
get: () => updateTrigger,
},
}), }),
); );
@ -270,6 +255,7 @@
// So, we need return it // So, we need return it
this.annotations = { this.annotations = {
exportDataset: Object.getPrototypeOf(this).annotations.exportDataset.bind(this), 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); const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.delete);
return result; 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( Object.defineProperties(
@ -336,6 +354,16 @@
); );
return result; 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, 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 // SPDX-License-Identifier: MIT
(() => { (() => {
const store = require('store');
const PluginRegistry = require('./plugins'); const PluginRegistry = require('./plugins');
const loggerStorage = require('./logger-storage'); const loggerStorage = require('./logger-storage');
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy');
@ -11,12 +10,11 @@
getFrame, getRanges, getPreview, clear: clearFrames, getContextImage, getFrame, getRanges, getPreview, clear: clearFrames, getContextImage,
} = require('./frames'); } = require('./frames');
const { ArgumentError, DataError } = require('./exceptions'); const { ArgumentError, DataError } = require('./exceptions');
const { TaskStatus } = require('./enums'); const { JobStage, JobState } = require('./enums');
const { Label } = require('./labels'); const { Label } = require('./labels');
const User = require('./user'); const User = require('./user');
const Issue = require('./issue'); const Issue = require('./issue');
const Review = require('./review'); const { FieldUpdateTrigger, checkObjectType } = require('./common');
const { FieldUpdateTrigger } = require('./common');
function buildDuplicatedAPI(prototype) { function buildDuplicatedAPI(prototype) {
Object.defineProperties(prototype, { Object.defineProperties(prototype, {
@ -180,11 +178,10 @@
const result = await PluginRegistry.apiWrapper.call(this, prototype.frames.preview); const result = await PluginRegistry.apiWrapper.call(this, prototype.frames.preview);
return result; return result;
}, },
async contextImage(taskId, frameId) { async contextImage(frameId) {
const result = await PluginRegistry.apiWrapper.call( const result = await PluginRegistry.apiWrapper.call(
this, this,
prototype.frames.contextImage, prototype.frames.contextImage,
taskId,
frameId, frameId,
); );
return result; return result;
@ -709,18 +706,21 @@
const data = { const data = {
id: undefined, id: undefined,
assignee: null, assignee: null,
reviewer: null, stage: undefined,
status: undefined, state: undefined,
start_frame: undefined, start_frame: undefined,
stop_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({ const updateTrigger = new FieldUpdateTrigger();
assignee: false,
reviewer: false,
status: false,
});
for (const property in data) { for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property)) { if (Object.prototype.hasOwnProperty.call(data, property)) {
@ -735,7 +735,19 @@
} }
if (data.assignee) data.assignee = new User(data.assignee); 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( Object.defineProperties(
this, this,
@ -764,42 +776,53 @@
if (assignee !== null && !(assignee instanceof User)) { if (assignee !== null && !(assignee instanceof User)) {
throw new ArgumentError('Value must be a user instance'); throw new ArgumentError('Value must be a user instance');
} }
updatedFields.assignee = true; updateTrigger.update('assignee');
data.assignee = assignee; data.assignee = assignee;
}, },
}, },
/** /**
* Instance of a user who is responsible for review * @name stage
* @name reviewer * @type {module:API.cvat.enums.JobStage}
* @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Job * @memberof module:API.cvat.classes.Job
* @instance * @instance
* @throws {module:API.cvat.exceptions.ArgumentError} * @throws {module:API.cvat.exceptions.ArgumentError}
*/ */
reviewer: { stage: {
get: () => data.reviewer, get: () => data.stage,
set: (reviewer) => { set: (stage) => {
if (reviewer !== null && !(reviewer instanceof User)) { const type = JobStage;
throw new ArgumentError('Value must be a user instance'); 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 * @name state
* @type {module:API.cvat.enums.TaskStatus} * @type {module:API.cvat.enums.JobState}
* @memberof module:API.cvat.classes.Job * @memberof module:API.cvat.classes.Job
* @instance * @instance
* @throws {module:API.cvat.exceptions.ArgumentError} * @throws {module:API.cvat.exceptions.ArgumentError}
*/ */
status: { state: {
get: () => data.status, get: () => data.state,
set: (status) => { set: (state) => {
const type = TaskStatus; const type = JobState;
let valueInEnum = false; let valueInEnum = false;
for (const value in type) { for (const value in type) {
if (type[value] === status) { if (type[value] === state) {
valueInEnum = true; valueInEnum = true;
break; break;
} }
@ -807,12 +830,12 @@
if (!valueInEnum) { if (!valueInEnum) {
throw new ArgumentError( 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; updateTrigger.update('state');
data.status = status; data.state = state;
}, },
}, },
/** /**
@ -836,17 +859,96 @@
get: () => data.stop_frame, get: () => data.stop_frame,
}, },
/** /**
* @name task * @name projectId
* @type {module:API.cvat.classes.Task} * @type {integer|null}
* @memberof module:API.cvat.classes.Job * @memberof module:API.cvat.classes.Job
* @readonly * @readonly
* @instance * @instance
*/ */
task: { projectId: {
get: () => data.task, 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: { _updateTrigger: {
get: () => updatedFields, get: () => updateTrigger,
}, },
}), }),
); );
@ -870,6 +972,7 @@
export: Object.getPrototypeOf(this).annotations.export.bind(this), export: Object.getPrototypeOf(this).annotations.export.bind(this),
statistics: Object.getPrototypeOf(this).annotations.statistics.bind(this), statistics: Object.getPrototypeOf(this).annotations.statistics.bind(this),
hasUnsavedChanges: Object.getPrototypeOf(this).annotations.hasUnsavedChanges.bind(this), hasUnsavedChanges: Object.getPrototypeOf(this).annotations.hasUnsavedChanges.bind(this),
exportDataset: Object.getPrototypeOf(this).annotations.exportDataset.bind(this),
}; };
this.actions = { 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 * @method save
* @memberof module:API.cvat.classes.Job * @memberof module:API.cvat.classes.Job
* @readonly * @readonly
@ -916,7 +1019,7 @@
* Method returns a list of issues for a job * Method returns a list of issues for a job
* @method issues * @method issues
* @memberof module:API.cvat.classes.Job * @memberof module:API.cvat.classes.Job
* @type {module:API.cvat.classes.Issue[]} * @returns {module:API.cvat.classes.Issue[]}
* @readonly * @readonly
* @instance * @instance
* @async * @async
@ -929,44 +1032,36 @@
} }
/** /**
* Method returns a list of reviews for a job * Method adds a new issue to a job
* @method reviews * @method openIssue
* @type {module:API.cvat.classes.Review[]}
* @memberof module:API.cvat.classes.Job * @memberof module:API.cvat.classes.Job
* @returns {module:API.cvat.classes.Issue}
* @param {module:API.cvat.classes.Issue} issue
* @param {string} message
* @readonly * @readonly
* @instance * @instance
* @async * @async
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.ServerError} * @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
*/ */
async reviews() { async openIssue(issue, message) {
const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.reviews); const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.openIssue, issue, message);
return result; return result;
} }
/** /**
* /** * Method removes all job related data from the client (annotations, history, etc.)
* @typedef {Object} ReviewSummary * @method close
* @property {number} reviews Number of done reviews * @returns {module:API.cvat.classes.Job}
* @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}
* @memberof module:API.cvat.classes.Job * @memberof module:API.cvat.classes.Job
* @readonly * @readonly
* @instance
* @async * @async
* @throws {module:API.cvat.exceptions.ServerError} * @instance
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
*/ */
async reviewsSummary() { async close() {
const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.reviewsSummary); const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.close);
return result; return result;
} }
} }
@ -993,7 +1088,7 @@
const data = { const data = {
id: undefined, id: undefined,
name: undefined, name: undefined,
project_id: undefined, project_id: null,
status: undefined, status: undefined,
size: undefined, size: undefined,
mode: undefined, mode: undefined,
@ -1017,16 +1112,10 @@
copy_data: undefined, copy_data: undefined,
dimension: undefined, dimension: undefined,
cloud_storage_id: undefined, cloud_storage_id: undefined,
sorting_method: undefined,
}; };
const updatedFields = new FieldUpdateTrigger({ const updateTrigger = new FieldUpdateTrigger();
name: false,
assignee: false,
bug_tracker: false,
subset: false,
labels: false,
project_id: false,
});
for (const property in data) { for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) { if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
@ -1045,6 +1134,13 @@
remote_files: [], 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)) { if (Array.isArray(initialData.segments)) {
for (const segment of initialData.segments) { for (const segment of initialData.segments) {
if (Array.isArray(segment.jobs)) { if (Array.isArray(segment.jobs)) {
@ -1053,25 +1149,28 @@
url: job.url, url: job.url,
id: job.id, id: job.id,
assignee: job.assignee, assignee: job.assignee,
reviewer: job.reviewer, state: job.state,
status: job.status, stage: job.stage,
start_frame: segment.start_frame, start_frame: segment.start_frame,
stop_frame: segment.stop_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); 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( Object.defineProperties(
this, this,
Object.freeze({ Object.freeze({
@ -1098,7 +1197,7 @@
if (!value.trim().length) { if (!value.trim().length) {
throw new ArgumentError('Value must not be empty'); throw new ArgumentError('Value must not be empty');
} }
updatedFields.name = true; updateTrigger.update('name');
data.name = value; data.name = value;
}, },
}, },
@ -1115,7 +1214,7 @@
throw new ArgumentError('Value must be a positive integer'); throw new ArgumentError('Value must be a positive integer');
} }
updatedFields.project_id = true; updateTrigger.update('projectId');
data.project_id = projectId; data.project_id = projectId;
}, },
}, },
@ -1174,7 +1273,7 @@
if (assignee !== null && !(assignee instanceof User)) { if (assignee !== null && !(assignee instanceof User)) {
throw new ArgumentError('Value must be a user instance'); throw new ArgumentError('Value must be a user instance');
} }
updatedFields.assignee = true; updateTrigger.update('assignee');
data.assignee = assignee; data.assignee = assignee;
}, },
}, },
@ -1214,7 +1313,7 @@
); );
} }
updatedFields.bug_tracker = true; updateTrigger.update('bugTracker');
data.bug_tracker = tracker; data.bug_tracker = tracker;
}, },
}, },
@ -1234,7 +1333,7 @@
); );
} }
updatedFields.subset = true; updateTrigger.update('subset');
data.subset = subset; data.subset = subset;
}, },
}, },
@ -1335,7 +1434,6 @@
}, },
}, },
/** /**
* After task has been created value can be appended only.
* @name labels * @name labels
* @type {module:API.cvat.classes.Label[]} * @type {module:API.cvat.classes.Label[]}
* @memberof module:API.cvat.classes.Task * @memberof module:API.cvat.classes.Task
@ -1363,7 +1461,7 @@
_label.deleted = true; _label.deleted = true;
}); });
updatedFields.labels = true; updateTrigger.update('labels');
data.labels = [...deletedLabels, ...labels]; data.labels = [...deletedLabels, ...labels];
}, },
}, },
@ -1530,14 +1628,14 @@
dataChunkType: { dataChunkType: {
get: () => data.data_compressed_chunk_type, get: () => data.data_compressed_chunk_type,
}, },
/**
* @name dimension
* @type {module:API.cvat.enums.DimensionType}
* @memberof module:API.cvat.classes.Task
* @readonly
* @instance
*/
dimension: { dimension: {
/**
* @name enabled
* @type {string}
* @memberof module:API.cvat.enums.DimensionType
* @readonly
* @instance
*/
get: () => data.dimension, get: () => data.dimension,
}, },
/** /**
@ -1549,11 +1647,21 @@
cloudStorageId: { cloudStorageId: {
get: () => data.cloud_storage_id, 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: { _internalData: {
get: () => data, get: () => data,
}, },
__updatedFields: { _updateTrigger: {
get: () => updatedFields, get: () => updateTrigger,
}, },
}), }),
); );
@ -1720,70 +1828,32 @@
Job.prototype.save.implementation = async function () { Job.prototype.save.implementation = async function () {
if (this.id) { if (this.id) {
const jobData = {}; const jobData = this._updateTrigger.getUpdated(this);
if (jobData.assignee) {
for (const [field, isUpdated] of Object.entries(this.__updatedFields)) { jobData.assignee = jobData.assignee.id;
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;
}
}
} }
await serverProxy.jobs.save(this.id, jobData); const data = await serverProxy.jobs.save(this.id, jobData);
this._updateTrigger.reset();
this.__updatedFields.reset(); return new Job(data);
return this;
} }
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 () { 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)); return result.map((issue) => new Issue(issue));
}; };
Job.prototype.reviews.implementation = async function () { Job.prototype.openIssue.implementation = async function (issue, message) {
const result = await serverProxy.jobs.reviews.get(this.id); checkObjectType('issue', issue, null, Issue);
const reviews = result.map((review) => new Review(review)); checkObjectType('message', message, 'string');
const result = await serverProxy.issues.create({
// try to get not finished review from the local storage ...issue.serialize(),
const data = store.get(`job-${this.id}-review`); message,
if (data) { });
reviews.push(new Review(JSON.parse(data))); return new Issue(result);
}
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.frames.get.implementation = async function (frame, isPlaying, step) { Job.prototype.frames.get.implementation = async function (frame, isPlaying, step) {
@ -1796,27 +1866,32 @@
} }
const frameData = await getFrame( const frameData = await getFrame(
this.task.id, this.taskId,
this.task.dataChunkSize, this.id,
this.task.dataChunkType, this.dataChunkSize,
this.task.mode, this.dataChunkType,
this.mode,
frame, frame,
this.startFrame, this.startFrame,
this.stopFrame, this.stopFrame,
isPlaying, isPlaying,
step, step,
this.task.dimension, this.dimension,
); );
return frameData; return frameData;
}; };
Job.prototype.frames.ranges.implementation = async function () { Job.prototype.frames.ranges.implementation = async function () {
const rangesData = await getRanges(this.task.id); const rangesData = await getRanges(this.taskId);
return rangesData; return rangesData;
}; };
Job.prototype.frames.preview.implementation = async function () { 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; return frameData;
}; };
@ -1939,7 +2014,7 @@
}; };
Job.prototype.annotations.exportDataset.implementation = async function (format, saveImages, customName) { 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; return result;
}; };
@ -1969,20 +2044,54 @@
}; };
Job.prototype.logger.log.implementation = async function (logType, payload, wait) { 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; return result;
}; };
Job.prototype.predictor.status.implementation = async function () { Job.prototype.predictor.status.implementation = async function () {
const result = await this.task.predictor.status(); if (!Number.isInteger(this.projectId)) {
return result; 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) { 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; 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() { Task.prototype.close.implementation = function closeTask() {
clearFrames(this.id); clearFrames(this.id);
for (const job of this.jobs) { for (const job of this.jobs) {
@ -1997,40 +2106,22 @@
// TODO: Add ability to change an owner and an assignee // TODO: Add ability to change an owner and an assignee
if (typeof this.id !== 'undefined') { if (typeof this.id !== 'undefined') {
// If the task has been already created, we update it // If the task has been already created, we update it
const taskData = {}; const taskData = this._updateTrigger.getUpdated(this, {
bugTracker: 'bug_tracker',
for (const [field, isUpdated] of Object.entries(this.__updatedFields)) { projectId: 'project_id',
if (isUpdated) { assignee: 'assignee_id',
switch (field) { });
case 'assignee': if (taskData.assignee_id) {
taskData.assignee_id = this.assignee ? this.assignee.id : null; taskData.assignee_id = taskData.assignee_id.id;
break; }
case 'name': if (taskData.labels) {
taskData.name = this.name; taskData.labels = this._internalData.labels;
break; taskData.labels = taskData.labels.map((el) => el.toJSON());
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;
}
}
} }
await serverProxy.tasks.saveTask(this.id, taskData); const data = await serverProxy.tasks.save(this.id, taskData);
this._updateTrigger.reset();
this.__updatedFields.reset(); return new Task(data);
return this;
} }
const taskSpec = { const taskSpec = {
@ -2061,6 +2152,7 @@
image_quality: this.imageQuality, image_quality: this.imageQuality,
use_zip_chunks: this.useZipChunks, use_zip_chunks: this.useZipChunks,
use_cache: this.useCache, use_cache: this.useCache,
sorting_method: this.sortingMethod,
}; };
if (typeof this.startFrame !== 'undefined') { if (typeof this.startFrame !== 'undefined') {
@ -2082,22 +2174,23 @@
taskDataSpec.cloud_storage_id = this.cloudStorageId; 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); return new Task(task);
}; };
Task.prototype.delete.implementation = async function () { Task.prototype.delete.implementation = async function () {
const result = await serverProxy.tasks.deleteTask(this.id); const result = await serverProxy.tasks.delete(this.id);
return result; return result;
}; };
Task.prototype.export.implementation = async function () { Task.prototype.export.implementation = async function () {
const result = await serverProxy.tasks.exportTask(this.id); const result = await serverProxy.tasks.export(this.id);
return result; return result;
}; };
Task.import.implementation = async function (file) { 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; return result;
}; };
@ -2112,6 +2205,7 @@
const result = await getFrame( const result = await getFrame(
this.id, this.id,
null,
this.dataChunkSize, this.dataChunkSize,
this.dataChunkType, this.dataChunkType,
this.mode, this.mode,
@ -2130,6 +2224,10 @@
}; };
Task.prototype.frames.preview.implementation = async function () { Task.prototype.frames.preview.implementation = async function () {
if (this.id === null) {
return '';
}
const frameData = await getPreview(this.id); const frameData = await getPreview(this.id);
return frameData; return frameData;
}; };
@ -2317,9 +2415,4 @@
const result = await serverProxy.predictor.predict(this.id, frame); const result = await serverProxy.predictor.predict(this.id, frame);
return result; 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 // SPDX-License-Identifier: MIT
@ -14,80 +14,88 @@
this, this,
Object.freeze({ Object.freeze({
/** /**
* Statistics by labels with a structure: * Statistics by labels with a structure:
* @example * @example
* { * {
* label: { * label: {
* boxes: { * boxes: {
* tracks: 10, * tracks: 10,
* shapes: 11, * shapes: 11,
* }, * },
* polygons: { * polygons: {
* tracks: 13, * tracks: 13,
* shapes: 14, * shapes: 14,
* }, * },
* polylines: { * polylines: {
* tracks: 16, * tracks: 16,
* shapes: 17, * shapes: 17,
* }, * },
* points: { * points: {
* tracks: 19, * tracks: 19,
* shapes: 20, * shapes: 20,
* }, * },
* cuboids: { * ellipse: {
* tracks: 21, * tracks: 13,
* shapes: 22, * shapes: 15,
* }, * },
* tags: 66, * cuboids: {
* manually: 186, * tracks: 21,
* interpolated: 500, * shapes: 22,
* total: 608, * },
* } * tags: 66,
* } * manually: 186,
* @name label * interpolated: 500,
* @type {Object} * total: 608,
* @memberof module:API.cvat.classes.Statistics * }
* @readonly * }
* @instance * @name label
*/ * @type {Object}
* @memberof module:API.cvat.classes.Statistics
* @readonly
* @instance
*/
label: { label: {
get: () => JSON.parse(JSON.stringify(label)), get: () => JSON.parse(JSON.stringify(label)),
}, },
/** /**
* Total statistics (covers all labels) with a structure: * Total statistics (covers all labels) with a structure:
* @example * @example
* { * {
* boxes: { * boxes: {
* tracks: 10, * tracks: 10,
* shapes: 11, * shapes: 11,
* }, * },
* polygons: { * polygons: {
* tracks: 13, * tracks: 13,
* shapes: 14, * shapes: 14,
* }, * },
* polylines: { * polylines: {
* tracks: 16, * tracks: 16,
* shapes: 17, * shapes: 17,
* }, * },
* points: { * points: {
* tracks: 19, * tracks: 19,
* shapes: 20, * shapes: 20,
* }, * },
* cuboids: { * ellipse: {
* tracks: 21, * tracks: 13,
* shapes: 22, * shapes: 15,
* }, * },
* tags: 66, * cuboids: {
* manually: 186, * tracks: 21,
* interpolated: 500, * shapes: 22,
* total: 608, * },
* } * tags: 66,
* @name total * manually: 186,
* @type {Object} * interpolated: 500,
* @memberof module:API.cvat.classes.Statistics * total: 608,
* @readonly * }
* @instance * @name total
*/ * @type {Object}
* @memberof module:API.cvat.classes.Statistics
* @readonly
* @instance
*/
total: { total: {
get: () => JSON.parse(JSON.stringify(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 // SPDX-License-Identifier: MIT
@ -174,10 +174,6 @@
email_verification_required: this.isVerified, email_verification_required: this.isVerified,
}; };
} }
toJSON() {
return this.serialize();
}
} }
module.exports = User; module.exports = User;

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -30,8 +30,8 @@ describe('Feature: get annotations', () => {
const annotations10 = await job.annotations.get(10); const annotations10 = await job.annotations.get(10);
expect(Array.isArray(annotations0)).toBeTruthy(); expect(Array.isArray(annotations0)).toBeTruthy();
expect(Array.isArray(annotations10)).toBeTruthy(); expect(Array.isArray(annotations10)).toBeTruthy();
expect(annotations0).toHaveLength(1); expect(annotations0).toHaveLength(2);
expect(annotations10).toHaveLength(2); expect(annotations10).toHaveLength(3);
for (const state of annotations0.concat(annotations10)) { for (const state of annotations0.concat(annotations10)) {
expect(state).toBeInstanceOf(window.cvat.classes.ObjectState); 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); 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', () => { describe('Feature: put annotations', () => {
@ -94,7 +144,29 @@ describe('Feature: put annotations', () => {
shapeType: window.cvat.enums.ObjectShape.RECTANGLE, shapeType: window.cvat.enums.ObjectShape.RECTANGLE,
points: [0, 0, 100, 100], points: [0, 0, 100, 100],
occluded: false, 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, zOrder: 0,
}); });
@ -138,7 +210,7 @@ describe('Feature: put annotations', () => {
shapeType: window.cvat.enums.ObjectShape.RECTANGLE, shapeType: window.cvat.enums.ObjectShape.RECTANGLE,
points: [0, 0, 100, 100], points: [0, 0, 100, 100],
occluded: false, occluded: false,
label: job.task.labels[0], label: job.labels[0],
zOrder: 0, zOrder: 0,
}); });
@ -388,7 +460,7 @@ describe('Feature: save annotations', () => {
shapeType: window.cvat.enums.ObjectShape.POLYGON, shapeType: window.cvat.enums.ObjectShape.POLYGON,
points: [0, 0, 100, 0, 100, 50], points: [0, 0, 100, 0, 100, 50],
occluded: true, occluded: true,
label: job.task.labels[0], label: job.labels[0],
zOrder: 0, zOrder: 0,
}); });
@ -562,7 +634,7 @@ describe('Feature: split annotations', () => {
await task.annotations.split(annotations5[0], 5); await task.annotations.split(annotations5[0], 5);
const splitted4 = await task.annotations.get(4); const splitted4 = await task.annotations.get(4);
const splitted5 = (await task.annotations.get(5)).filter((state) => !state.outside); 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 () => { test('split annotations in a job', async () => {
@ -574,7 +646,7 @@ describe('Feature: split annotations', () => {
await job.annotations.split(annotations5[0], 5); await job.annotations.split(annotations5[0], 5);
const splitted4 = await job.annotations.get(4); const splitted4 = await job.annotations.get(4);
const splitted5 = (await job.annotations.get(5)).filter((state) => !state.outside); 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 () => { test('split on a bad frame', async () => {
@ -702,7 +774,7 @@ describe('Feature: get statistics', () => {
await job.annotations.clear(true); await job.annotations.clear(true);
const statistics = await job.annotations.statistics(); const statistics = await job.annotations.statistics();
expect(statistics).toBeInstanceOf(window.cvat.classes.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); 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 // SPDX-License-Identifier: MIT
@ -36,7 +36,7 @@ describe('Feature: get cloud storages', () => {
expect(cloudStorage.id).toBe(1); expect(cloudStorage.id).toBe(1);
expect(cloudStorage.providerType).toBe('AWS_S3_BUCKET'); expect(cloudStorage.providerType).toBe('AWS_S3_BUCKET');
expect(cloudStorage.credentialsType).toBe('KEY_SECRET_KEY_PAIR'); 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.displayName).toBe('Demonstration bucket');
expect(cloudStorage.manifests).toHaveLength(1); expect(cloudStorage.manifests).toHaveLength(1);
expect(cloudStorage.manifests[0]).toBe('manifest.jsonl'); expect(cloudStorage.manifests[0]).toBe('manifest.jsonl');
@ -61,24 +61,18 @@ describe('Feature: get cloud storages', () => {
}); });
test('get cloud storages by filters', async () => { test('get cloud storages by filters', async () => {
const filters = new Map([ const filter = {
['providerType', 'AWS_S3_BUCKET'], and: [
['resourceName', 'bucket'], { '==': [{ var: 'display_name' }, 'Demonstration bucket'] },
['displayName', 'Demonstration bucket'], { '==': [{ var: 'resource_name' }, 'bucket'] },
['credentialsType', 'KEY_SECRET_KEY_PAIR'], { '==': [{ var: 'description' }, 'It is first bucket'] },
['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(Object.fromEntries(filters)); };
const [cloudStorage] = result; const result = await window.cvat.cloudStorages.get({ filter: JSON.stringify(filter) });
expect(Array.isArray(result)).toBeTruthy(); expect(result).toBeInstanceOf(Array);
expect(result).toHaveLength(1);
expect(cloudStorage).toBeInstanceOf(CloudStorage);
expect(cloudStorage.id).toBe(1);
filters.forEach((value, key) => {
expect(cloudStorage[key]).toBe(value);
});
}); });
test('get cloud storage by invalid filters', async () => { 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 // SPDX-License-Identifier: MIT
@ -25,8 +25,8 @@ describe('Feature: get a list of jobs', () => {
expect(el).toBeInstanceOf(Job); expect(el).toBeInstanceOf(Job);
} }
expect(result[0].task.id).toBe(3); expect(result[0].taskId).toBe(3);
expect(result[0].task).toBe(result[1].task); expect(result[0].taskId).toBe(result[1].taskId);
}); });
test('get jobs by an unknown task id', async () => { test('get jobs by an unknown task id', async () => {
@ -89,18 +89,17 @@ describe('Feature: get a list of jobs', () => {
}); });
describe('Feature: save job', () => { describe('Feature: save job', () => {
test('save status of a job', async () => { test('save stage and state of a job', async () => {
let result = await window.cvat.jobs.get({ const result = await window.cvat.jobs.get({
jobID: 1, jobID: 1,
}); });
result[0].status = 'validation'; result[0].stage = 'validation';
await result[0].save(); result[0].state = 'new';
const newJob = await result[0].save();
result = await window.cvat.jobs.get({ expect(newJob.stage).toBe('validation');
jobID: 1, expect(newJob.state).toBe('new');
});
expect(result[0].status).toBe('validation');
}); });
test('save invalid status of a job', async () => { test('save invalid status of a job', async () => {
@ -108,9 +107,11 @@ describe('Feature: save job', () => {
jobID: 1, jobID: 1,
}); });
await result[0].save();
expect(() => { expect(() => {
result[0].status = 'invalid'; result[0].state = 'invalid';
}).toThrow(window.cvat.exceptions.ArgumentError);
expect(() => {
result[0].stage = 'invalid';
}).toThrow(window.cvat.exceptions.ArgumentError); }).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 // SPDX-License-Identifier: MIT
@ -11,12 +11,11 @@ jest.mock('../../src/server-proxy', () => {
// Initialize api // Initialize api
window.cvat = require('../../src/api'); window.cvat = require('../../src/api');
const { Task } = require('../../src/session');
const { Project } = require('../../src/project'); const { Project } = require('../../src/project');
describe('Feature: get projects', () => { describe('Feature: get projects', () => {
test('get all projects', async () => { 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(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(2); expect(result).toHaveLength(2);
for (const el of result) { for (const el of result) {
@ -33,8 +32,8 @@ describe('Feature: get projects', () => {
expect(result).toHaveLength(1); expect(result).toHaveLength(1);
expect(result[0]).toBeInstanceOf(Project); expect(result[0]).toBeInstanceOf(Project);
expect(result[0].id).toBe(2); expect(result[0].id).toBe(2);
expect(result[0].tasks).toHaveLength(1); // eslint-disable-next-line no-underscore-dangle
expect(result[0].tasks[0]).toBeInstanceOf(Task); expect(result[0]._internalData.task_ids).toHaveLength(1);
}); });
test('get a project by an unknown id', async () => { test('get a project by an unknown id', async () => {
@ -55,16 +54,12 @@ describe('Feature: get projects', () => {
test('get projects by filters', async () => { test('get projects by filters', async () => {
const result = await window.cvat.projects.get({ const result = await window.cvat.projects.get({
status: 'completed', filter: '{"and":[{"==":[{"var":"status"},"completed"]}]}',
}); });
expect(Array.isArray(result)).toBeTruthy(); expect(result).toBeInstanceOf(Array);
expect(result).toHaveLength(1);
expect(result[0]).toBeInstanceOf(Project);
expect(result[0].id).toBe(2);
expect(result[0].status).toBe('completed');
}); });
test('get projects by invalid filters', async () => { test('get projects by invalid query', async () => {
expect( expect(
window.cvat.projects.get({ window.cvat.projects.get({
unknown: '5', unknown: '5',

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

@ -55,7 +55,7 @@ const usersDummyData = {
previous: null, previous: null,
results: [ results: [
{ {
url: 'http://localhost:7000/api/v1/users/1', url: 'http://localhost:7000/api/users/1',
id: 1, id: 1,
username: 'admin', username: 'admin',
first_name: '', first_name: '',
@ -69,7 +69,7 @@ const usersDummyData = {
date_joined: '2019-05-13T15:33:17.833200+03:00', 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, id: 2,
username: 'bsekache', username: 'bsekache',
first_name: '', first_name: '',
@ -149,18 +149,18 @@ const projectsDummyData = {
previous: null, previous: null,
results: [ results: [
{ {
url: 'http://192.168.0.139:7000/api/v1/projects/6', url: 'http://192.168.0.139:7000/api/projects/6',
id: 6, id: 6,
name: 'Some empty project', name: 'Some empty project',
labels: [], labels: [],
tasks: [], tasks: [],
owner: { owner: {
url: 'http://localhost:7000/api/v1/users/2', url: 'http://localhost:7000/api/users/2',
id: 2, id: 2,
username: 'bsekache', username: 'bsekache',
}, },
assignee: { assignee: {
url: 'http://localhost:7000/api/v1/users/2', url: 'http://localhost:7000/api/users/2',
id: 2, id: 2,
username: 'bsekache', username: 'bsekache',
}, },
@ -170,7 +170,7 @@ const projectsDummyData = {
status: 'annotation', 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, id: 2,
name: 'Test project with roads', name: 'Test project with roads',
labels: [ labels: [
@ -198,13 +198,13 @@ const projectsDummyData = {
], ],
tasks: [ tasks: [
{ {
url: 'http://192.168.0.139:7000/api/v1/tasks/2', url: 'http://192.168.0.139:7000/api/tasks/2',
id: 2, id: 2,
name: 'road 1', name: 'road 1',
project_id: 1, project_id: 1,
mode: 'interpolation', mode: 'interpolation',
owner: { owner: {
url: 'http://localhost:7000/api/v1/users/1', url: 'http://localhost:7000/api/users/1',
id: 1, id: 1,
username: 'admin', username: 'admin',
}, },
@ -239,11 +239,12 @@ const projectsDummyData = {
stop_frame: 99, stop_frame: 99,
jobs: [ jobs: [
{ {
url: 'http://192.168.0.139:7000/api/v1/jobs/1', url: 'http://192.168.0.139:7000/api/jobs/1',
id: 1, id: 1,
assignee: null, assignee: null,
reviewer: null,
status: 'completed', status: 'completed',
stage: 'acceptance',
state: 'completed',
}, },
], ],
}, },
@ -252,11 +253,12 @@ const projectsDummyData = {
stop_frame: 194, stop_frame: 194,
jobs: [ jobs: [
{ {
url: 'http://192.168.0.139:7000/api/v1/jobs/2', url: 'http://192.168.0.139:7000/api/jobs/2',
id: 2, id: 2,
assignee: null, assignee: null,
reviewer: null,
status: 'completed', status: 'completed',
stage: 'acceptance',
state: 'completed',
}, },
], ],
}, },
@ -265,11 +267,12 @@ const projectsDummyData = {
stop_frame: 289, stop_frame: 289,
jobs: [ jobs: [
{ {
url: 'http://192.168.0.139:7000/api/v1/jobs/3', url: 'http://192.168.0.139:7000/api/jobs/3',
id: 3, id: 3,
assignee: null, assignee: null,
reviewer: null,
status: 'completed', status: 'completed',
stage: 'acceptance',
state: 'completed',
}, },
], ],
}, },
@ -278,11 +281,12 @@ const projectsDummyData = {
stop_frame: 384, stop_frame: 384,
jobs: [ jobs: [
{ {
url: 'http://192.168.0.139:7000/api/v1/jobs/4', url: 'http://192.168.0.139:7000/api/jobs/4',
id: 4, id: 4,
assignee: null, assignee: null,
reviewer: null,
status: 'completed', status: 'completed',
stage: 'acceptance',
state: 'completed',
}, },
], ],
}, },
@ -291,11 +295,12 @@ const projectsDummyData = {
stop_frame: 431, stop_frame: 431,
jobs: [ jobs: [
{ {
url: 'http://192.168.0.139:7000/api/v1/jobs/5', url: 'http://192.168.0.139:7000/api/jobs/5',
id: 5, id: 5,
assignee: null, assignee: null,
reviewer: null,
status: 'completed', status: 'completed',
stage: 'acceptance',
state: 'completed',
}, },
], ],
}, },
@ -309,7 +314,7 @@ const projectsDummyData = {
}, },
], ],
owner: { owner: {
url: 'http://localhost:7000/api/v1/users/1', url: 'http://localhost:7000/api/users/1',
id: 1, id: 1,
username: 'admin', username: 'admin',
}, },
@ -328,13 +333,13 @@ const tasksDummyData = {
previous: null, previous: null,
results: [ results: [
{ {
url: 'http://localhost:7000/api/v1/tasks/102', url: 'http://localhost:7000/api/tasks/102',
id: 102, id: 102,
name: 'Test', name: 'Test',
size: 1, size: 1,
mode: 'annotation', mode: 'annotation',
owner: { owner: {
url: 'http://localhost:7000/api/v1/users/1', url: 'http://localhost:7000/api/users/1',
id: 1, id: 1,
username: 'admin', username: 'admin',
}, },
@ -344,6 +349,9 @@ const tasksDummyData = {
updated_date: '2019-09-05T14:04:07.569344Z', updated_date: '2019-09-05T14:04:07.569344Z',
overlap: 0, overlap: 0,
segment_size: 0, segment_size: 0,
dimension: '2d',
data_compressed_chunk_type: 'imageset',
data_chunk_size: 1,
status: 'annotation', status: 'annotation',
labels: [ labels: [
{ {
@ -358,11 +366,12 @@ const tasksDummyData = {
stop_frame: 0, stop_frame: 0,
jobs: [ jobs: [
{ {
url: 'http://localhost:7000/api/v1/jobs/112', url: 'http://localhost:7000/api/jobs/112',
id: 112, id: 112,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -373,13 +382,13 @@ const tasksDummyData = {
frame_filter: '', frame_filter: '',
}, },
{ {
url: 'http://localhost:7000/api/v1/tasks/100', url: 'http://localhost:7000/api/tasks/100',
id: 100, id: 100,
name: 'Image Task', name: 'Image Task',
size: 9, size: 9,
mode: 'annotation', mode: 'annotation',
owner: { owner: {
url: 'http://localhost:7000/api/v1/users/1', url: 'http://localhost:7000/api/users/1',
id: 1, id: 1,
username: 'admin', username: 'admin',
}, },
@ -389,6 +398,9 @@ const tasksDummyData = {
updated_date: '2019-07-16T15:51:29.142871+03:00', updated_date: '2019-07-16T15:51:29.142871+03:00',
overlap: 0, overlap: 0,
segment_size: 0, segment_size: 0,
dimension: '2d',
data_compressed_chunk_type: 'imageset',
data_chunk_size: 1,
status: 'annotation', status: 'annotation',
labels: [ labels: [
{ {
@ -408,11 +420,12 @@ const tasksDummyData = {
stop_frame: 8, stop_frame: 8,
jobs: [ jobs: [
{ {
url: 'http://localhost:7000/api/v1/jobs/100', url: 'http://localhost:7000/api/jobs/100',
id: 100, id: 100,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -423,13 +436,13 @@ const tasksDummyData = {
frame_filter: '', frame_filter: '',
}, },
{ {
url: 'http://localhost:7000/api/v1/tasks/10', url: 'http://localhost:7000/api/tasks/10',
id: 101, id: 101,
name: 'Video Task', name: 'Video Task',
size: 5002, size: 5002,
mode: 'interpolation', mode: 'interpolation',
owner: { owner: {
url: 'http://localhost:7000/api/v1/users/1', url: 'http://localhost:7000/api/users/1',
id: 1, id: 1,
username: 'admin', username: 'admin',
}, },
@ -439,6 +452,9 @@ const tasksDummyData = {
updated_date: '2019-07-12T16:43:58.904892+03:00', updated_date: '2019-07-12T16:43:58.904892+03:00',
overlap: 5, overlap: 5,
segment_size: 500, segment_size: 500,
dimension: '2d',
data_compressed_chunk_type: 'imageset',
data_chunk_size: 1,
status: 'annotation', status: 'annotation',
labels: [ labels: [
{ {
@ -612,11 +628,12 @@ const tasksDummyData = {
stop_frame: 499, stop_frame: 499,
jobs: [ jobs: [
{ {
url: 'http://localhost:7000/api/v1/jobs/10', url: 'http://localhost:7000/api/jobs/10',
id: 101, id: 101,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -625,11 +642,12 @@ const tasksDummyData = {
stop_frame: 994, stop_frame: 994,
jobs: [ jobs: [
{ {
url: 'http://localhost:7000/api/v1/jobs/11', url: 'http://localhost:7000/api/jobs/11',
id: 102, id: 102,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -638,11 +656,12 @@ const tasksDummyData = {
stop_frame: 1489, stop_frame: 1489,
jobs: [ jobs: [
{ {
url: 'http://localhost:7000/api/v1/jobs/12', url: 'http://localhost:7000/api/jobs/12',
id: 103, id: 103,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -651,11 +670,12 @@ const tasksDummyData = {
stop_frame: 1984, stop_frame: 1984,
jobs: [ jobs: [
{ {
url: 'http://localhost:7000/api/v1/jobs/13', url: 'http://localhost:7000/api/jobs/13',
id: 104, id: 104,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -664,11 +684,12 @@ const tasksDummyData = {
stop_frame: 2479, stop_frame: 2479,
jobs: [ jobs: [
{ {
url: 'http://localhost:7000/api/v1/jobs/14', url: 'http://localhost:7000/api/jobs/14',
id: 105, id: 105,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -677,11 +698,12 @@ const tasksDummyData = {
stop_frame: 2974, stop_frame: 2974,
jobs: [ jobs: [
{ {
url: 'http://localhost:7000/api/v1/jobs/15', url: 'http://localhost:7000/api/jobs/15',
id: 106, id: 106,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -690,11 +712,12 @@ const tasksDummyData = {
stop_frame: 3469, stop_frame: 3469,
jobs: [ jobs: [
{ {
url: 'http://localhost:7000/api/v1/jobs/16', url: 'http://localhost:7000/api/jobs/16',
id: 107, id: 107,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -703,11 +726,12 @@ const tasksDummyData = {
stop_frame: 3964, stop_frame: 3964,
jobs: [ jobs: [
{ {
url: 'http://localhost:7000/api/v1/jobs/17', url: 'http://localhost:7000/api/jobs/17',
id: 108, id: 108,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -716,11 +740,12 @@ const tasksDummyData = {
stop_frame: 4459, stop_frame: 4459,
jobs: [ jobs: [
{ {
url: 'http://localhost:7000/api/v1/jobs/18', url: 'http://localhost:7000/api/jobs/18',
id: 109, id: 109,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -729,11 +754,12 @@ const tasksDummyData = {
stop_frame: 4954, stop_frame: 4954,
jobs: [ jobs: [
{ {
url: 'http://localhost:7000/api/v1/jobs/19', url: 'http://localhost:7000/api/jobs/19',
id: 110, id: 110,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -742,11 +768,12 @@ const tasksDummyData = {
stop_frame: 5001, stop_frame: 5001,
jobs: [ jobs: [
{ {
url: 'http://localhost:7000/api/v1/jobs/20', url: 'http://localhost:7000/api/jobs/20',
id: 111, id: 111,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -757,13 +784,13 @@ const tasksDummyData = {
frame_filter: '', frame_filter: '',
}, },
{ {
url: 'http://localhost:7000/api/v1/tasks/3', url: 'http://localhost:7000/api/tasks/3',
id: 3, id: 3,
name: 'Test Task', name: 'Test Task',
size: 5002, size: 5002,
mode: 'interpolation', mode: 'interpolation',
owner: { owner: {
url: 'http://localhost:7000/api/v1/users/2', url: 'http://localhost:7000/api/users/2',
id: 2, id: 2,
username: 'bsekache', username: 'bsekache',
}, },
@ -773,7 +800,9 @@ const tasksDummyData = {
updated_date: '2019-05-16T13:08:00.621797+03:00', updated_date: '2019-05-16T13:08:00.621797+03:00',
overlap: 5, overlap: 5,
segment_size: 5000, segment_size: 5000,
flipped: false, dimension: '2d',
data_compressed_chunk_type: 'imageset',
data_chunk_size: 1,
status: 'annotation', status: 'annotation',
labels: [ labels: [
{ {
@ -947,11 +976,12 @@ const tasksDummyData = {
stop_frame: 4999, stop_frame: 4999,
jobs: [ jobs: [
{ {
url: 'http://localhost:7000/api/v1/jobs/3', url: 'http://localhost:7000/api/jobs/3',
id: 3, id: 3,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -960,11 +990,12 @@ const tasksDummyData = {
stop_frame: 5001, stop_frame: 5001,
jobs: [ jobs: [
{ {
url: 'http://localhost:7000/api/v1/jobs/4', url: 'http://localhost:7000/api/jobs/4',
id: 4, id: 4,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -972,13 +1003,13 @@ const tasksDummyData = {
image_quality: 50, image_quality: 50,
}, },
{ {
url: 'http://localhost:7000/api/v1/tasks/2', url: 'http://localhost:7000/api/tasks/2',
id: 2, id: 2,
name: 'Video', name: 'Video',
size: 75, size: 75,
mode: 'interpolation', mode: 'interpolation',
owner: { owner: {
url: 'http://localhost:7000/api/v1/users/1', url: 'http://localhost:7000/api/users/1',
id: 1, id: 1,
username: 'admin', username: 'admin',
}, },
@ -989,7 +1020,9 @@ const tasksDummyData = {
updated_date: '2019-05-15T16:58:27.992785+03:00', updated_date: '2019-05-15T16:58:27.992785+03:00',
overlap: 5, overlap: 5,
segment_size: 0, segment_size: 0,
flipped: false, dimension: '2d',
data_compressed_chunk_type: 'imageset',
data_chunk_size: 1,
status: 'annotation', status: 'annotation',
labels: [ labels: [
{ {
@ -1163,11 +1196,12 @@ const tasksDummyData = {
stop_frame: 74, stop_frame: 74,
jobs: [ jobs: [
{ {
url: 'http://localhost:7000/api/v1/jobs/2', url: 'http://localhost:7000/api/jobs/2',
id: 2, id: 2,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -1175,13 +1209,13 @@ const tasksDummyData = {
image_quality: 50, image_quality: 50,
}, },
{ {
url: 'http://localhost:7000/api/v1/tasks/1', url: 'http://localhost:7000/api/tasks/1',
id: 1, id: 1,
name: 'Labels Set', name: 'Labels Set',
size: 9, size: 9,
mode: 'annotation', mode: 'annotation',
owner: { owner: {
url: 'http://localhost:7000/api/v1/users/1', url: 'http://localhost:7000/api/users/1',
id: 1, id: 1,
username: 'admin', username: 'admin',
}, },
@ -1191,7 +1225,9 @@ const tasksDummyData = {
updated_date: '2019-05-15T11:20:55.770587+03:00', updated_date: '2019-05-15T11:20:55.770587+03:00',
overlap: 0, overlap: 0,
segment_size: 0, segment_size: 0,
flipped: false, dimension: '2d',
data_compressed_chunk_type: 'imageset',
data_chunk_size: 1,
status: 'annotation', status: 'annotation',
labels: [ labels: [
{ {
@ -1365,11 +1401,12 @@ const tasksDummyData = {
stop_frame: 8, stop_frame: 8,
jobs: [ jobs: [
{ {
url: 'http://localhost:7000/api/v1/jobs/1', url: 'http://localhost:7000/api/jobs/1',
id: 1, id: 1,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', 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: { 101: {
version: 21, version: 21,
tags: [], tags: [],
@ -1740,6 +1845,7 @@ const taskAnnotationsDummyData = {
occluded: false, occluded: false,
z_order: 1, z_order: 1,
points: [425.58984375, 540.298828125, 755.9765625, 745.6328125], points: [425.58984375, 540.298828125, 755.9765625, 745.6328125],
rotation: 0,
id: 379, id: 379,
frame: 0, frame: 0,
outside: false, outside: false,
@ -1759,6 +1865,7 @@ const taskAnnotationsDummyData = {
occluded: false, occluded: false,
z_order: 1, z_order: 1,
points: [238.8000000000011, 498.6000000000022, 546.01171875, 660.720703125], points: [238.8000000000011, 498.6000000000022, 546.01171875, 660.720703125],
rotation: 100,
id: 380, id: 380,
frame: 10, frame: 10,
outside: false, outside: false,
@ -1769,6 +1876,7 @@ const taskAnnotationsDummyData = {
occluded: false, occluded: false,
z_order: 1, z_order: 1,
points: [13.3955078125, 447.650390625, 320.6072265624989, 609.7710937499978], points: [13.3955078125, 447.650390625, 320.6072265624989, 609.7710937499978],
rotation: 340,
id: 381, id: 381,
frame: 20, frame: 20,
outside: false, 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: { 100: {
@ -2548,14 +2688,35 @@ const frameMetaDummyData = {
}; };
const cloudStoragesDummyData = { const cloudStoragesDummyData = {
count: 2, count: 3,
next: null, next: null,
previous: null, previous: null,
results: [ 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, id: 2,
owner: { owner: {
url: 'http://localhost:7000/api/v1/users/1', url: 'http://localhost:7000/api/users/1',
id: 1, id: 1,
username: 'maya', username: 'maya',
first_name: '', first_name: '',
@ -2576,7 +2737,7 @@ const cloudStoragesDummyData = {
{ {
id: 1, id: 1,
owner: { owner: {
url: 'http://localhost:7000/api/v1/users/1', url: 'http://localhost:7000/api/users/1',
id: 1, id: 1,
username: 'maya', username: 'maya',
first_name: '', first_name: '',

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

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

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

@ -1,7 +1,17 @@
server { server {
root /usr/share/nginx/html; root /usr/share/nginx/html;
# Any route that doesn't have a file extension (e.g. /devices)
location / { location / {
# Any route that doesn't exist on the server (e.g. /devices)
try_files $uri $uri/ /index.html; 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 // SPDX-License-Identifier: MIT
@ -6,10 +6,12 @@ import {
ActionCreator, AnyAction, Dispatch, Store, ActionCreator, AnyAction, Dispatch, Store,
} from 'redux'; } from 'redux';
import { ThunkAction } from 'utils/redux'; import { ThunkAction } from 'utils/redux';
import isAbleToChangeFrame from 'utils/is-able-to-change-frame';
import { RectDrawingMethod, CuboidDrawingMethod, Canvas } from 'cvat-canvas-wrapper'; import { RectDrawingMethod, CuboidDrawingMethod, Canvas } from 'cvat-canvas-wrapper';
import getCore from 'cvat-core-wrapper'; import getCore from 'cvat-core-wrapper';
import logger, { LogType } from 'cvat-logger'; import logger, { LogType } from 'cvat-logger';
import { getCVATStore } from 'cvat-store'; import { getCVATStore } from 'cvat-store';
import { import {
ActiveControl, ActiveControl,
CombinedState, CombinedState,
@ -24,6 +26,8 @@ import {
Task, Task,
Workspace, Workspace,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
import { updateJobAsync } from './tasks-actions';
import { switchToolsBlockerState } from './settings-actions';
interface AnnotationsParameters { interface AnnotationsParameters {
filters: string[]; filters: string[];
@ -183,8 +187,6 @@ export enum AnnotationActionTypes {
SAVE_LOGS_FAILED = 'SAVE_LOGS_FAILED', SAVE_LOGS_FAILED = 'SAVE_LOGS_FAILED',
INTERACT_WITH_CANVAS = 'INTERACT_WITH_CANVAS', INTERACT_WITH_CANVAS = 'INTERACT_WITH_CANVAS',
GET_DATA_FAILED = 'GET_DATA_FAILED', 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', SET_FORCE_EXIT_ANNOTATION_PAGE_FLAG = 'SET_FORCE_EXIT_ANNOTATION_PAGE_FLAG',
UPDATE_PREDICTOR_STATE = 'UPDATE_PREDICTOR_STATE', UPDATE_PREDICTOR_STATE = 'UPDATE_PREDICTOR_STATE',
GET_PREDICTIONS = 'GET_PREDICTIONS', GET_PREDICTIONS = 'GET_PREDICTIONS',
@ -343,7 +345,7 @@ export function uploadJobAnnotationsAsync(job: any, loader: any, file: File): Th
const state: CombinedState = getStore().getState(); const state: CombinedState = getStore().getState();
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters(); 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'); throw Error('Annotations is being uploaded for the task');
} }
if (state.annotation.activities.loads[job.id]) { if (state.annotation.activities.loads[job.id]) {
@ -639,7 +641,7 @@ export function getPredictionsAsync(): ThunkAction {
annotations = annotations.map( annotations = annotations.map(
(data: any): any => new cvat.classes.ObjectState({ (data: any): any => new cvat.classes.ObjectState({
shapeType: data.type, 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, points: data.points,
objectType: ObjectType.SHAPE, objectType: ObjectType.SHAPE,
frame, frame,
@ -692,8 +694,8 @@ export function changeFrameAsync(
frameStep?: number, frameStep?: number,
forceUpdate?: boolean, forceUpdate?: boolean,
): ThunkAction { ): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>, getState: () => CombinedState): Promise<void> => {
const state: CombinedState = getStore().getState(); const state: CombinedState = getState();
const { instance: job } = state.annotation.job; const { instance: job } = state.annotation.job;
const { filters, frame, showAllInterpolationTracks } = receiveAnnotationsParameters(); const { filters, frame, showAllInterpolationTracks } = receiveAnnotationsParameters();
@ -702,37 +704,52 @@ export function changeFrameAsync(
throw Error(`Required frame ${toFrame} is out of the current job`); throw Error(`Required frame ${toFrame} is out of the current job`);
} }
if (toFrame === frame && !forceUpdate) { const abortAction = (): AnyAction => {
dispatch({ const currentState = getState();
return ({
type: AnnotationActionTypes.CHANGE_FRAME_SUCCESS, type: AnnotationActionTypes.CHANGE_FRAME_SUCCESS,
payload: { payload: {
number: state.annotation.player.frame.number, number: currentState.annotation.player.frame.number,
data: state.annotation.player.frame.data, data: currentState.annotation.player.frame.data,
filename: state.annotation.player.frame.filename, filename: currentState.annotation.player.frame.filename,
hasRelatedContext: state.annotation.player.frame.hasRelatedContext, hasRelatedContext: currentState.annotation.player.frame.hasRelatedContext,
delay: state.annotation.player.frame.delay, delay: currentState.annotation.player.frame.delay,
changeTime: state.annotation.player.frame.changeTime, changeTime: currentState.annotation.player.frame.changeTime,
states: state.annotation.annotations.states, states: currentState.annotation.annotations.states,
minZ: state.annotation.annotations.zLayer.min, minZ: currentState.annotation.annotations.zLayer.min,
maxZ: state.annotation.annotations.zLayer.max, maxZ: currentState.annotation.annotations.zLayer.max,
curZ: state.annotation.annotations.zLayer.cur, curZ: currentState.annotation.annotations.zLayer.cur,
}, },
}); });
};
return;
}
// Start async requests
dispatch({ dispatch({
type: AnnotationActionTypes.CHANGE_FRAME, type: AnnotationActionTypes.CHANGE_FRAME,
payload: {}, 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, { await job.logger.log(LogType.changeFrame, {
from: frame, from: frame,
to: toFrame, 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 [minZ, maxZ] = computeZRange(states);
const currentTime = new Date().getTime(); const currentTime = new Date().getTime();
let frameSpeed; let frameSpeed;
@ -950,7 +967,7 @@ export function closeJob(): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const { jobInstance } = receiveAnnotationsParameters(); const { jobInstance } = receiveAnnotationsParameters();
if (jobInstance) { if (jobInstance) {
await jobInstance.task.close(); await jobInstance.close();
} }
dispatch({ dispatch({
@ -960,9 +977,9 @@ export function closeJob(): ThunkAction {
} }
export function getJobAsync(tid: number, jid: number, initialFrame: number, initialFilters: object[]): 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 { try {
const state: CombinedState = getStore().getState(); const state = getState();
const filters = initialFilters; const filters = initialFilters;
const { const {
settings: { settings: {
@ -986,20 +1003,18 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init
true, true,
); );
// Check state if the task is already there // Check if the task was already downloaded to the state
let task = state.tasks.current let job: any | null = null;
const [task] = state.tasks.current
.filter((_task: Task) => _task.instance.id === tid) .filter((_task: Task) => _task.instance.id === tid)
.map((_task: Task) => _task.instance)[0]; .map((_task: Task) => _task.instance);
if (task) {
// If there aren't the task, get it from the server [job] = task.jobs.filter((_job: any) => _job.id === jid);
if (!task) { if (!job) {
[task] = await cvat.tasks.get({ id: tid }); throw new Error(`Task ${tid} doesn't contain the job ${jid}`);
} }
} else {
// Finally get the job from the task [job] = await cvat.jobs.get({ jobID: jid });
const job = task.jobs.filter((_job: any) => _job.id === jid)[0];
if (!job) {
throw new Error(`Task ${tid} doesn't contain the job ${jid}`);
} }
const frameNumber = Math.max(Math.min(job.stopFrame, initialFrame), job.startFrame); 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 states = await job.annotations.get(frameNumber, showAllInterpolationTracks, filters);
const issues = await job.issues(); const issues = await job.issues();
const reviews = await job.reviews();
const [minZ, maxZ] = computeZRange(states); const [minZ, maxZ] = computeZRange(states);
const colors = [...cvat.enums.colors]; const colors = [...cvat.enums.colors];
@ -1031,7 +1045,6 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init
openTime, openTime,
job, job,
issues, issues,
reviews,
states, states,
frameNumber, frameNumber,
frameFilename: frameData.filename, 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; const workspace = Workspace.STANDARD3D;
dispatch(changeWorkspace(workspace)); dispatch(changeWorkspace(workspace));
} }
const updatePredictorStatus = async (): Promise<void> => { const updatePredictorStatus = async (): Promise<void> => {
// get current job // get current job
const currentState: CombinedState = getStore().getState(); const currentState: CombinedState = getState();
const { openTime: currentOpenTime, instance: currentJob } = currentState.annotation.job; const { openTime: currentOpenTime, instance: currentJob } = currentState.annotation.job;
if (currentJob === null || currentJob.id !== job.id || currentOpenTime !== openTime) { if (currentJob === null || currentJob.id !== job.id || currentOpenTime !== openTime) {
// the job was closed, changed or reopened // 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(); updatePredictorStatus();
} }
@ -1120,6 +1133,11 @@ export function saveAnnotationsAsync(sessionInstance: any, afterSave?: () => voi
afterSave(); afterSave();
} }
if (sessionInstance instanceof cvat.classes.Job && sessionInstance.state === cvat.enums.JobState.NEW) {
sessionInstance.state = cvat.enums.JobState.IN_PROGRESS;
dispatch(updateJobAsync(sessionInstance));
}
dispatch({ dispatch({
type: AnnotationActionTypes.SAVE_ANNOTATIONS_SUCCESS, type: AnnotationActionTypes.SAVE_ANNOTATIONS_SUCCESS,
payload: { payload: {
@ -1488,12 +1506,13 @@ export function repeatDrawShapeAsync(): ThunkAction {
let activeControl = ActiveControl.CURSOR; let activeControl = ActiveControl.CURSOR;
if (activeInteractor && canvasInstance instanceof Canvas) { if (activeInteractor && canvasInstance instanceof Canvas) {
if (activeInteractor.type === 'tracker') { if (activeInteractor.type.includes('tracker')) {
canvasInstance.interact({ canvasInstance.interact({
enabled: true, enabled: true,
shapeType: 'rectangle', shapeType: 'rectangle',
}); });
dispatch(interactWithCanvas(activeInteractor, activeLabelID)); dispatch(interactWithCanvas(activeInteractor, activeLabelID));
dispatch(switchToolsBlockerState({ buttonVisible: false }));
} else { } else {
canvasInstance.interact({ canvasInstance.interact({
enabled: true, enabled: true,
@ -1515,7 +1534,10 @@ export function repeatDrawShapeAsync(): ThunkAction {
activeControl = ActiveControl.DRAW_POLYLINE; activeControl = ActiveControl.DRAW_POLYLINE;
} else if (activeShapeType === ShapeType.CUBOID) { } else if (activeShapeType === ShapeType.CUBOID) {
activeControl = ActiveControl.DRAW_CUBOID; activeControl = ActiveControl.DRAW_CUBOID;
} else if (activeShapeType === ShapeType.ELLIPSE) {
activeControl = ActiveControl.DRAW_ELLIPSE;
} }
dispatch({ dispatch({
type: AnnotationActionTypes.REPEAT_DRAW_SHAPE, type: AnnotationActionTypes.REPEAT_DRAW_SHAPE,
payload: { payload: {
@ -1533,14 +1555,14 @@ export function repeatDrawShapeAsync(): ThunkAction {
frame: frameNumber, frame: frameNumber,
}); });
dispatch(createAnnotationsAsync(jobInstance, frameNumber, [objectState])); dispatch(createAnnotationsAsync(jobInstance, frameNumber, [objectState]));
} else { } else if (canvasInstance) {
canvasInstance.draw({ canvasInstance.draw({
enabled: true, enabled: true,
rectDrawingMethod: activeRectDrawingMethod, rectDrawingMethod: activeRectDrawingMethod,
cuboidDrawingMethod: activeCuboidDrawingMethod, cuboidDrawingMethod: activeCuboidDrawingMethod,
numberOfPoints: activeNumOfPoints, numberOfPoints: activeNumOfPoints,
shapeType: activeShapeType, 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, enabled: true,
redraw: activatedStateID, redraw: activatedStateID,
shapeType: state.shapeType, 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 { export function setForceExitAnnotationFlag(forceExit: boolean): AnyAction {
return { return {
type: AnnotationActionTypes.SET_FORCE_EXIT_ANNOTATION_PAGE_FLAG, type: AnnotationActionTypes.SET_FORCE_EXIT_ANNOTATION_PAGE_FLAG,
@ -1645,7 +1649,7 @@ export function getContextImageAsync(): ThunkAction {
payload: {}, payload: {},
}); });
const contextImageData = await job.frames.contextImage(job.task.id, frameNumber); const contextImageData = await job.frames.contextImage(frameNumber);
dispatch({ dispatch({
type: AnnotationActionTypes.GET_CONTEXT_IMAGE_SUCCESS, type: AnnotationActionTypes.GET_CONTEXT_IMAGE_SUCCESS,
payload: { contextImageData }, payload: { contextImageData },

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

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -103,6 +103,13 @@ export type CloudStorageActions = ActionUnion<typeof cloudStoragesActions>;
export function getCloudStoragesAsync(query: Partial<CloudStoragesQuery>): ThunkAction { export function getCloudStoragesAsync(query: Partial<CloudStoragesQuery>): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { 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.getCloudStorages());
dispatch(cloudStoragesActions.updateCloudStoragesGettingQuery(query)); 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; let result = null;
try { try {
result = await cvat.cloudStorages.get(filteredQuery); 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> => { return async (dispatch): Promise<void> => {
try { 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 => { const dispatchCallback = (action: ModelsActions): void => {
dispatch(action); dispatch(action);
}; };
listen( listen(
{ {
taskID: taskInstance.id, taskID: taskId,
requestID, requestID,
}, },
dispatchCallback, dispatchCallback,
); );
} catch (error) { } 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 // SPDX-License-Identifier: MIT
import { Dispatch, ActionCreator } from 'redux'; import { Dispatch, ActionCreator } from 'redux';
import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import { ProjectsQuery } from 'reducers/interfaces'; import { ProjectsQuery, TasksQuery, CombinedState } from 'reducers/interfaces';
import { getTasksSuccess, updateTaskSuccess } from 'actions/tasks-actions'; import { getTasksAsync } from 'actions/tasks-actions';
import { getCVATStore } from 'cvat-store';
import getCore from 'cvat-core-wrapper'; import getCore from 'cvat-core-wrapper';
const cvat = getCore(); const cvat = getCore();
@ -25,6 +26,12 @@ export enum ProjectsActionTypes {
DELETE_PROJECT = 'DELETE_PROJECT', DELETE_PROJECT = 'DELETE_PROJECT',
DELETE_PROJECT_SUCCESS = 'DELETE_PROJECT_SUCCESS', DELETE_PROJECT_SUCCESS = 'DELETE_PROJECT_SUCCESS',
DELETE_PROJECT_FAILED = 'DELETE_PROJECT_FAILED', 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 // prettier-ignore
@ -34,8 +41,8 @@ const projectActions = {
createAction(ProjectsActionTypes.GET_PROJECTS_SUCCESS, { array, previews, count }) createAction(ProjectsActionTypes.GET_PROJECTS_SUCCESS, { array, previews, count })
), ),
getProjectsFailed: (error: any) => createAction(ProjectsActionTypes.GET_PROJECTS_FAILED, { error }), getProjectsFailed: (error: any) => createAction(ProjectsActionTypes.GET_PROJECTS_FAILED, { error }),
updateProjectsGettingQuery: (query: Partial<ProjectsQuery>) => ( updateProjectsGettingQuery: (query: Partial<ProjectsQuery>, tasksQuery: Partial<TasksQuery> = {}) => (
createAction(ProjectsActionTypes.UPDATE_PROJECTS_GETTING_QUERY, { query }) createAction(ProjectsActionTypes.UPDATE_PROJECTS_GETTING_QUERY, { query, tasksQuery })
), ),
createProject: () => createAction(ProjectsActionTypes.CREATE_PROJECT), createProject: () => createAction(ProjectsActionTypes.CREATE_PROJECT),
createProjectSuccess: (projectId: number) => ( createProjectSuccess: (projectId: number) => (
@ -54,14 +61,45 @@ const projectActions = {
deleteProjectFailed: (projectId: number, error: any) => ( deleteProjectFailed: (projectId: number, error: any) => (
createAction(ProjectsActionTypes.DELETE_PROJECT_FAILED, { projectId, error }) 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 type ProjectActions = ActionUnion<typeof projectActions>;
export function getProjectsAsync(query: Partial<ProjectsQuery>): ThunkAction { export function getProjectTasksAsync(tasksQuery: Partial<TasksQuery> = {}): ThunkAction<void> {
return async (dispatch: ActionCreator<Dispatch>, getState): Promise<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.getProjects());
dispatch(projectActions.updateProjectsGettingQuery(query)); dispatch(projectActions.updateProjectsGettingQuery(query, tasksQuery));
// Clear query object from null fields // Clear query object from null fields
const filteredQuery: Partial<ProjectsQuery> = { 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; let result = null;
try { try {
result = await cvat.projects.get(filteredQuery); result = await cvat.projects.get(filteredQuery);
@ -85,38 +140,15 @@ export function getProjectsAsync(query: Partial<ProjectsQuery>): ThunkAction {
const array = Array.from(result); const array = Array.from(result);
// Appropriate tasks fetching proccess needs with retrieving only a single project const previewPromises = array.map((project): string => (project as any).preview().catch(() => ''));
if (Object.keys(filteredQuery).includes('id')) { dispatch(projectActions.getProjectsSuccess(array, await Promise.all(previewPromises), result.count));
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();
dispatch(projectActions.getProjectsSuccess(array, taskPreviews, result.count)); // Appropriate tasks fetching proccess needs with retrieving only a single project
if (Object.keys(filteredQuery).includes('id') && typeof filteredQuery.id === 'number') {
if (!state.tasks.fetching) { dispatch(getProjectTasksAsync({
dispatch( ...tasksQuery,
getTasksSuccess(tasks, taskPreviews, tasks.length, { projectId: filteredQuery.id,
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));
} }
}; };
} }
@ -136,17 +168,14 @@ export function createProjectAsync(data: any): ThunkAction {
} }
export function updateProjectAsync(projectInstance: any): ThunkAction { export function updateProjectAsync(projectInstance: any): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch, getState): Promise<void> => {
try { try {
const state = getState();
dispatch(projectActions.updateProject()); dispatch(projectActions.updateProject());
await projectInstance.save(); await projectInstance.save();
const [project] = await cvat.projects.get({ id: projectInstance.id }); 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)); dispatch(projectActions.updateProjectSuccess(project));
project.tasks.forEach((task: any) => { dispatch(getProjectTasksAsync(state.projects.tasksGettingQuery));
dispatch(updateTaskSuccess(task, task.id));
});
} catch (error) { } catch (error) {
let project = null; let project = null;
try { 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 { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import getCore from 'cvat-core-wrapper'; import getCore from 'cvat-core-wrapper';
import { updateTaskSuccess } from './tasks-actions';
const cvat = getCore(); const cvat = getCore();
export enum ReviewActionTypes { export enum ReviewActionTypes {
INITIALIZE_REVIEW_SUCCESS = 'INITIALIZE_REVIEW_SUCCESS',
INITIALIZE_REVIEW_FAILED = 'INITIALIZE_REVIEW_FAILED',
CREATE_ISSUE = 'CREATE_ISSUE', CREATE_ISSUE = 'CREATE_ISSUE',
START_ISSUE = 'START_ISSUE', START_ISSUE = 'START_ISSUE',
FINISH_ISSUE_SUCCESS = 'FINISH_ISSUE_SUCCESS', FINISH_ISSUE_SUCCESS = 'FINISH_ISSUE_SUCCESS',
@ -25,17 +22,16 @@ export enum ReviewActionTypes {
COMMENT_ISSUE = 'COMMENT_ISSUE', COMMENT_ISSUE = 'COMMENT_ISSUE',
COMMENT_ISSUE_SUCCESS = 'COMMENT_ISSUE_SUCCESS', COMMENT_ISSUE_SUCCESS = 'COMMENT_ISSUE_SUCCESS',
COMMENT_ISSUE_FAILED = 'COMMENT_ISSUE_FAILED', COMMENT_ISSUE_FAILED = 'COMMENT_ISSUE_FAILED',
REMOVE_ISSUE_SUCCESS = 'REMOVE_ISSUE_SUCCESS',
REMOVE_ISSUE_FAILED = 'REMOVE_ISSUE_FAILED',
SUBMIT_REVIEW = 'SUBMIT_REVIEW', SUBMIT_REVIEW = 'SUBMIT_REVIEW',
SUBMIT_REVIEW_SUCCESS = 'SUBMIT_REVIEW_SUCCESS', SUBMIT_REVIEW_SUCCESS = 'SUBMIT_REVIEW_SUCCESS',
SUBMIT_REVIEW_FAILED = 'SUBMIT_REVIEW_FAILED', SUBMIT_REVIEW_FAILED = 'SUBMIT_REVIEW_FAILED',
SWITCH_ISSUES_HIDDEN_FLAG = 'SWITCH_ISSUES_HIDDEN_FLAG', SWITCH_ISSUES_HIDDEN_FLAG = 'SWITCH_ISSUES_HIDDEN_FLAG',
SWITCH_RESOLVED_ISSUES_HIDDEN_FLAG = 'SWITCH_RESOLVED_ISSUES_HIDDEN_FLAG',
} }
export const reviewActions = { 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, {}), createIssue: () => createAction(ReviewActionTypes.CREATE_ISSUE, {}),
startIssue: (position: number[]) => ( startIssue: (position: number[]) => (
createAction(ReviewActionTypes.START_ISSUE, { position: cvat.classes.Issue.hull(position) }) 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 }), reopenIssue: (issueId: number) => createAction(ReviewActionTypes.REOPEN_ISSUE, { issueId }),
reopenIssueSuccess: () => createAction(ReviewActionTypes.REOPEN_ISSUE_SUCCESS), reopenIssueSuccess: () => createAction(ReviewActionTypes.REOPEN_ISSUE_SUCCESS),
reopenIssueFailed: (error: any) => createAction(ReviewActionTypes.REOPEN_ISSUE_FAILED, { error }), 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), 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 }), 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 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) => { export const finishIssueAsync = (message: string): ThunkAction => async (dispatch, getState) => {
const state = getState(); const state = getState();
const { const {
auth: { user },
annotation: { annotation: {
player: { player: {
frame: { number: frameNumber }, frame: { number: frameNumber },
}, },
job: {
instance: jobInstance,
},
}, },
review: { activeReview, newIssuePosition }, review: { newIssuePosition },
} = state; } = state;
try { try {
const issue = await activeReview.openIssue({ const issue = new cvat.classes.Issue({
job: jobInstance.id,
frame: frameNumber, frame: frameNumber,
position: newIssuePosition, 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) { } catch (error) {
dispatch(reviewActions.finishIssueFailed(error)); dispatch(reviewActions.finishIssueFailed(error));
} }
@ -124,7 +99,7 @@ export const commentIssueAsync = (id: number, message: string): ThunkAction => a
const state = getState(); const state = getState();
const { const {
auth: { user }, auth: { user },
review: { frameIssues, activeReview }, review: { frameIssues },
} = state; } = state;
try { try {
@ -132,11 +107,9 @@ export const commentIssueAsync = (id: number, message: string): ThunkAction => a
const [issue] = frameIssues.filter((_issue: any): boolean => _issue.id === id); const [issue] = frameIssues.filter((_issue: any): boolean => _issue.id === id);
await issue.comment({ await issue.comment({
message, message,
author: user, owner: user,
}); });
if (activeReview && activeReview.issues.includes(issue)) {
await activeReview.toLocalStorage();
}
dispatch(reviewActions.commentIssueSuccess()); dispatch(reviewActions.commentIssueSuccess());
} catch (error) { } catch (error) {
dispatch(reviewActions.commentIssueFailed(error)); dispatch(reviewActions.commentIssueFailed(error));
@ -147,17 +120,13 @@ export const resolveIssueAsync = (id: number): ThunkAction => async (dispatch, g
const state = getState(); const state = getState();
const { const {
auth: { user }, auth: { user },
review: { frameIssues, activeReview }, review: { frameIssues },
} = state; } = state;
try { try {
dispatch(reviewActions.resolveIssue(id)); dispatch(reviewActions.resolveIssue(id));
const [issue] = frameIssues.filter((_issue: any): boolean => _issue.id === id); const [issue] = frameIssues.filter((_issue: any): boolean => _issue.id === id);
await issue.resolve(user); await issue.resolve(user);
if (activeReview && activeReview.issues.includes(issue)) {
await activeReview.toLocalStorage();
}
dispatch(reviewActions.resolveIssueSuccess()); dispatch(reviewActions.resolveIssueSuccess());
} catch (error) { } catch (error) {
dispatch(reviewActions.resolveIssueFailed(error)); dispatch(reviewActions.resolveIssueFailed(error));
@ -168,39 +137,35 @@ export const reopenIssueAsync = (id: number): ThunkAction => async (dispatch, ge
const state = getState(); const state = getState();
const { const {
auth: { user }, auth: { user },
review: { frameIssues, activeReview }, review: { frameIssues },
} = state; } = state;
try { try {
dispatch(reviewActions.reopenIssue(id)); dispatch(reviewActions.reopenIssue(id));
const [issue] = frameIssues.filter((_issue: any): boolean => _issue.id === id); const [issue] = frameIssues.filter((_issue: any): boolean => _issue.id === id);
await issue.reopen(user); await issue.reopen(user);
if (activeReview && activeReview.issues.includes(issue)) {
await activeReview.toLocalStorage();
}
dispatch(reviewActions.reopenIssueSuccess()); dispatch(reviewActions.reopenIssueSuccess());
} catch (error) { } catch (error) {
dispatch(reviewActions.reopenIssueFailed(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 state = getState();
const { const {
review: { frameIssues },
annotation: { annotation: {
job: { instance: jobInstance }, player: {
frame: { number: frameNumber },
},
}, },
} = state; } = state;
try { try {
dispatch(reviewActions.submitReview(review.id)); const [issue] = frameIssues.filter((_issue: any): boolean => _issue.id === id);
await review.submit(jobInstance.id); await issue.delete();
dispatch(reviewActions.removeIssueSuccess(id, frameNumber));
const [task] = await cvat.tasks.get({ id: jobInstance.task.id });
dispatch(updateTaskSuccess(task, jobInstance.task.id));
dispatch(reviewActions.submitReviewSuccess());
} catch (error) { } 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_STEP = 'CHANGE_FRAME_STEP',
CHANGE_FRAME_SPEED = 'CHANGE_FRAME_SPEED', CHANGE_FRAME_SPEED = 'CHANGE_FRAME_SPEED',
SWITCH_RESET_ZOOM = 'SWITCH_RESET_ZOOM', 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_BRIGHTNESS_LEVEL = 'CHANGE_BRIGHTNESS_LEVEL',
CHANGE_CONTRAST_LEVEL = 'CHANGE_CONTRAST_LEVEL', CHANGE_CONTRAST_LEVEL = 'CHANGE_CONTRAST_LEVEL',
CHANGE_SATURATION_LEVEL = 'CHANGE_SATURATION_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 { export function changeBrightnessLevel(level: number): AnyAction {
return { return {
type: SettingsActionTypes.CHANGE_BRIGHTNESS_LEVEL, 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 // SPDX-License-Identifier: MIT
@ -28,6 +28,9 @@ export enum TasksActionTypes {
UPDATE_TASK = 'UPDATE_TASK', UPDATE_TASK = 'UPDATE_TASK',
UPDATE_TASK_SUCCESS = 'UPDATE_TASK_SUCCESS', UPDATE_TASK_SUCCESS = 'UPDATE_TASK_SUCCESS',
UPDATE_TASK_FAILED = 'UPDATE_TASK_FAILED', 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', HIDE_EMPTY_TASKS = 'HIDE_EMPTY_TASKS',
EXPORT_TASK = 'EXPORT_TASK', EXPORT_TASK = 'EXPORT_TASK',
EXPORT_TASK_SUCCESS = 'EXPORT_TASK_SUCCESS', EXPORT_TASK_SUCCESS = 'EXPORT_TASK_SUCCESS',
@ -38,36 +41,34 @@ export enum TasksActionTypes {
SWITCH_MOVE_TASK_MODAL_VISIBLE = 'SWITCH_MOVE_TASK_MODAL_VISIBLE', SWITCH_MOVE_TASK_MODAL_VISIBLE = 'SWITCH_MOVE_TASK_MODAL_VISIBLE',
} }
function getTasks(): AnyAction { function getTasks(query: TasksQuery): AnyAction {
const action = { const action = {
type: TasksActionTypes.GET_TASKS, type: TasksActionTypes.GET_TASKS,
payload: {}, payload: {
query,
},
}; };
return action; 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 = { const action = {
type: TasksActionTypes.GET_TASKS_SUCCESS, type: TasksActionTypes.GET_TASKS_SUCCESS,
payload: { payload: {
previews, previews,
array, array,
count, count,
query,
}, },
}; };
return action; return action;
} }
function getTasksFailed(error: any, query: TasksQuery): AnyAction { function getTasksFailed(error: any): AnyAction {
const action = { const action = {
type: TasksActionTypes.GET_TASKS_FAILED, type: TasksActionTypes.GET_TASKS_FAILED,
payload: { payload: { error },
error,
query,
},
}; };
return action; return action;
@ -75,7 +76,7 @@ function getTasksFailed(error: any, query: TasksQuery): AnyAction {
export function getTasksAsync(query: TasksQuery): ThunkAction<Promise<void>, {}, {}, AnyAction> { export function getTasksAsync(query: TasksQuery): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch(getTasks()); dispatch(getTasks(query));
// We need remove all keys with null values from query // We need remove all keys with null values from query
const filteredQuery = { ...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; let result = null;
try { try {
result = await cvat.tasks.get(filteredQuery); result = await cvat.tasks.get(filteredQuery);
} catch (error) { } catch (error) {
dispatch(getTasksFailed(error, query)); dispatch(getTasksFailed(error));
return; return;
} }
@ -98,7 +116,7 @@ export function getTasksAsync(query: TasksQuery): ThunkAction<Promise<void>, {},
dispatch(getInferenceStatusAsync()); 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(); downloadAnchor.click();
dispatch(exportTaskSuccess(taskInstance.id)); dispatch(exportTaskSuccess(taskInstance.id));
} catch (error) { } 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, image_quality: 70,
use_zip_chunks: data.advanced.useZipChunks, use_zip_chunks: data.advanced.useZipChunks,
use_cache: data.advanced.useCache, use_cache: data.advanced.useCache,
sorting_method: data.advanced.sortingMethod,
}; };
if (data.projectId) { if (data.projectId) {
@ -411,8 +430,8 @@ export function createTaskAsync(data: any): ThunkAction<Promise<void>, {}, {}, A
dispatch(createTask()); dispatch(createTask());
try { try {
const savedTask = await taskInstance.save((status: string): void => { const savedTask = await taskInstance.save((status: string, progress: number): void => {
dispatch(createTaskUpdateStatus(status)); dispatch(createTaskUpdateStatus(status + (progress !== null ? ` ${Math.floor(progress * 100)}%` : '')));
}); });
dispatch(createTaskSuccess(savedTask.id)); dispatch(createTaskSuccess(savedTask.id));
} catch (error) { } catch (error) {
@ -439,6 +458,33 @@ export function updateTaskSuccess(task: any, taskID: number): AnyAction {
return action; 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 { function updateTaskFailed(error: any, task: any): AnyAction {
const action = { const action = {
type: TasksActionTypes.UPDATE_TASK_FAILED, 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> { 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 { try {
dispatch(updateTask()); dispatch(updateTask());
const currentUser = getState().auth.user; const task = await taskInstance.save();
await taskInstance.save(); dispatch(updateTaskSuccess(task, taskInstance.id));
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));
}
} catch (error) { } catch (error) {
// try abort all changes // try abort all changes
let task = null; 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> { export function updateJobAsync(jobInstance: any): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try { try {
dispatch(updateTask()); dispatch(updateJob());
await jobInstance.save(); const newJob = await jobInstance.save();
const [task] = await cvat.tasks.get({ id: jobInstance.task.id }); dispatch(updateJobSuccess(newJob));
dispatch(updateTaskSuccess(task, jobInstance.task.id));
} catch (error) { } catch (error) {
// try abort all changes dispatch(updateJobFailed(jobInstance.id, error));
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));
} }
}; };
} }

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